package project

import (
	"testing"

	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/tspath"
	"github.com/microsoft/typescript-go/internal/vfs/vfstest"
	"gotest.tools/v3/assert"
)

func TestProcessChanges(t *testing.T) {
	t.Parallel()
	// Helper to create test overlayFS
	createOverlayFS := func() *overlayFS {
		testFS := vfstest.FromMap(map[string]string{
			"/test1.ts": "// existing content",
			"/test2.ts": "// existing content",
		}, false /* useCaseSensitiveFileNames */)
		return newOverlayFS(
			testFS,
			make(map[tspath.Path]*Overlay),
			lsproto.PositionEncodingKindUTF16,
			func(fileName string) tspath.Path {
				return tspath.Path(fileName)
			},
		)
	}

	// Test URI constants
	const (
		testURI1 = lsproto.DocumentUri("file:///test1.ts")
		testURI2 = lsproto.DocumentUri("file:///test2.ts")
	)

	t.Run("multiple opens should panic", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		changes := []FileChange{
			{
				Kind:         FileChangeKindOpen,
				URI:          testURI1,
				Version:      1,
				Content:      "const x = 1;",
				LanguageKind: lsproto.LanguageKindTypeScript,
			},
			{
				Kind:         FileChangeKindOpen,
				URI:          testURI2,
				Version:      1,
				Content:      "const y = 2;",
				LanguageKind: lsproto.LanguageKindTypeScript,
			},
		}

		assert.Assert(t, func() (panicked bool) {
			defer func() {
				if r := recover(); r != nil {
					panicked = true
				}
			}()
			fs.processChanges(changes)
			return false
		}())
	})

	t.Run("watch create then delete becomes nothing", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		changes := []FileChange{
			{
				Kind: FileChangeKindWatchCreate,
				URI:  testURI1,
			},
			{
				Kind: FileChangeKindWatchDelete,
				URI:  testURI1,
			},
		}

		result, _ := fs.processChanges(changes)
		assert.Assert(t, result.IsEmpty())
	})

	t.Run("watch delete then create becomes change", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		changes := []FileChange{
			{
				Kind: FileChangeKindWatchDelete,
				URI:  testURI1,
			},
			{
				Kind: FileChangeKindWatchCreate,
				URI:  testURI1,
			},
		}

		result, _ := fs.processChanges(changes)

		assert.Equal(t, result.Created.Len(), 0)
		assert.Equal(t, result.Deleted.Len(), 0)
		assert.Assert(t, result.Changed.Has(testURI1))
	})

	t.Run("multiple watch changes deduplicated", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		changes := []FileChange{
			{
				Kind: FileChangeKindWatchChange,
				URI:  testURI1,
			},
			{
				Kind: FileChangeKindWatchChange,
				URI:  testURI1,
			},
			{
				Kind: FileChangeKindWatchChange,
				URI:  testURI1,
			},
		}

		result, _ := fs.processChanges(changes)

		assert.Assert(t, result.Changed.Has(testURI1))
		assert.Equal(t, result.Changed.Len(), 1)
	})

	t.Run("save marks overlay as matching disk", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		// First create an overlay
		fs.processChanges([]FileChange{
			{
				Kind:         FileChangeKindOpen,
				URI:          testURI1,
				Version:      1,
				Content:      "const x = 1;",
				LanguageKind: lsproto.LanguageKindTypeScript,
			},
		})
		// Then save
		result, _ := fs.processChanges([]FileChange{
			{
				Kind: FileChangeKindSave,
				URI:  testURI1,
			},
		})
		// We don't observe saves for snapshot changes,
		// so they're not included in the summary
		assert.Assert(t, result.IsEmpty())

		// Check that the overlay is marked as matching disk text
		fh := fs.getFile(testURI1.FileName())
		assert.Assert(t, fh != nil)
		assert.Assert(t, fh.MatchesDiskText())
	})

	t.Run("open falls back to file extension for unknown language kind", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()
		uri := lsproto.DocumentUri("file:///test1.mts")

		fs.processChanges([]FileChange{
			{
				Kind:         FileChangeKindOpen,
				URI:          uri,
				Version:      1,
				Content:      "export const x = 1;",
				LanguageKind: lsproto.LanguageKind("mts"),
			},
		})

		fh := fs.getFile(uri.FileName())
		assert.Assert(t, fh != nil)
		assert.Equal(t, fh.Kind(), core.ScriptKindTS)
	})

	t.Run("watch change on overlay marks as not matching disk", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		// First create an overlay
		fs.processChanges([]FileChange{
			{
				Kind:         FileChangeKindOpen,
				URI:          testURI1,
				Version:      1,
				Content:      "const x = 1;",
				LanguageKind: lsproto.LanguageKindTypeScript,
			},
		})
		assert.Assert(t, !fs.getFile(testURI1.FileName()).MatchesDiskText())

		// Then save
		fs.processChanges([]FileChange{
			{
				Kind: FileChangeKindSave,
				URI:  testURI1,
			},
		})
		assert.Assert(t, fs.getFile(testURI1.FileName()).MatchesDiskText())

		// Now process a watch change
		fs.processChanges([]FileChange{
			{
				Kind: FileChangeKindWatchChange,
				URI:  testURI1,
			},
		})
		assert.Assert(t, !fs.getFile(testURI1.FileName()).MatchesDiskText())
	})

	t.Run("save without overlay should not panic", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		// Save a file that was never opened (no overlay exists).
		// This can happen when an editor sends didSave for a file
		// that is not managed by the LSP server (e.g., package.json).
		result, _ := fs.processChanges([]FileChange{
			{
				Kind: FileChangeKindSave,
				URI:  testURI1,
			},
		})
		// Should be treated as a disk change
		assert.Assert(t, result.Changed.Has(testURI1))
	})

	t.Run("close then open in same batch marks as changed", func(t *testing.T) {
		t.Parallel()
		fs := createOverlayFS()

		// First create an overlay
		fs.processChanges([]FileChange{
			{
				Kind:         FileChangeKindOpen,
				URI:          testURI1,
				Version:      1,
				Content:      "const x = 1;",
				LanguageKind: lsproto.LanguageKindTypeScript,
			},
		})

		// Now close and reopen in the same batch (like Neovim does for file reload)
		result, _ := fs.processChanges([]FileChange{
			{
				Kind: FileChangeKindClose,
				URI:  testURI1,
			},
			{
				Kind:         FileChangeKindOpen,
				URI:          testURI1,
				Version:      0,
				Content:      "const x = 2;",
				LanguageKind: lsproto.LanguageKindTypeScript,
			},
		})

		// Should not be marked as opened since it was already open
		assert.Assert(t, result.Opened == "", "close then open should not mark as opened")
		// Should also be marked as changed since it was closed and reopened
		assert.Assert(t, result.Changed.Has(testURI1), "close then open should mark as changed")
		// Should have the new content
		fh := fs.getFile(testURI1.FileName())
		assert.Equal(t, fh.Content(), "const x = 2;")
	})
}
