package project_test

import (
	"context"
	"maps"
	"strings"
	"testing"

	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/bundled"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/glob"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/projecttestutil"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
	"gotest.tools/v3/assert"
)

func TestSession(t *testing.T) {
	t.Parallel()
	if !bundled.Embedded {
		t.Skip("bundled files are not embedded")
	}

	defaultFiles := map[string]any{
		"/home/projects/TS/p1/tsconfig.json": `{
			"compilerOptions": {
				"noLib": true,
				"module": "nodenext",
				"strict": true
			},
			"include": ["src"]
		}`,
		"/home/projects/TS/p1/src/index.ts": `import { x } from "./x";`,
		"/home/projects/TS/p1/src/x.ts":     `export const x = 1;`,
		"/home/projects/TS/p1/config.ts":    `let x = 1, y = 2;`,
	}

	t.Run("DidOpenFile", func(t *testing.T) {
		t.Parallel()
		t.Run("create configured project", func(t *testing.T) {
			t.Parallel()
			session, _ := projecttestutil.Setup(defaultFiles)
			snapshot, release := session.Snapshot()
			defer release()
			assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			snapshot, release = session.Snapshot()
			defer release()
			assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)

			configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json"))
			assert.Assert(t, configuredProject != nil)

			// Get language service to access the program
			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program := ls.GetProgram()
			assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil)
			assert.Equal(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "export const x = 1;")
		})

		t.Run("create inferred project", func(t *testing.T) {
			t.Parallel()
			session, _ := projecttestutil.Setup(defaultFiles)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/config.ts", 1, defaultFiles["/home/projects/TS/p1/config.ts"].(string), lsproto.LanguageKindTypeScript)

			// Find tsconfig, load, notice config.ts is not included, create inferred project
			snapshot, release := session.Snapshot()
			defer release()
			assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2)

			// Should have both configured project (for tsconfig.json) and inferred project
			configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json"))
			inferredProject := snapshot.ProjectCollection.InferredProject()
			assert.Assert(t, configuredProject != nil)
			assert.Assert(t, inferredProject != nil)
		})

		t.Run("inferred project for in-memory files", func(t *testing.T) {
			t.Parallel()
			session, _ := projecttestutil.Setup(defaultFiles)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/config.ts", 1, defaultFiles["/home/projects/TS/p1/config.ts"].(string), lsproto.LanguageKindTypeScript)
			session.DidOpenFile(context.Background(), "untitled:Untitled-1", 1, "x", lsproto.LanguageKindTypeScript)
			session.DidOpenFile(context.Background(), "untitled:Untitled-2", 1, "y", lsproto.LanguageKindTypeScript)

			snapshot, release := session.Snapshot()
			defer release()

			assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
			assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil)
		})

		t.Run("inferred project JS file", func(t *testing.T) {
			t.Parallel()
			jsFiles := map[string]any{
				"/home/projects/TS/p1/index.js": `import { x } from "./x";`,
			}
			session, _ := projecttestutil.Setup(jsFiles)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/index.js", 1, jsFiles["/home/projects/TS/p1/index.js"].(string), lsproto.LanguageKindJavaScript)

			snapshot, release := session.Snapshot()
			defer release()
			assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)

			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.js")
			assert.NilError(t, err)
			program := ls.GetProgram()
			assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/index.js") != nil)
		})
	})

	t.Run("DidChangeFile", func(t *testing.T) {
		t.Parallel()
		t.Run("update file and program", func(t *testing.T) {
			t.Parallel()
			session, _ := projecttestutil.Setup(defaultFiles)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)

			lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
			assert.NilError(t, err)
			programBefore := lsBefore.GetProgram()

			session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{
				{
					Partial: ptrTo(lsproto.TextDocumentContentChangePartial{
						Range: lsproto.Range{
							Start: lsproto.Position{
								Line:      0,
								Character: 17,
							},
							End: lsproto.Position{
								Line:      0,
								Character: 18,
							},
						},
						Text: "2",
					}),
				},
			})

			lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
			assert.NilError(t, err)
			programAfter := lsAfter.GetProgram()

			// Program should change due to the file content change
			assert.Check(t, programAfter != programBefore)
			assert.Equal(t, programAfter.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "export const x = 2;")
		})

		t.Run("unchanged source files are reused", func(t *testing.T) {
			t.Parallel()
			session, _ := projecttestutil.Setup(defaultFiles)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)

			lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
			assert.NilError(t, err)
			programBefore := lsBefore.GetProgram()
			indexFileBefore := programBefore.GetSourceFile("/home/projects/TS/p1/src/index.ts")

			session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{
				{
					Partial: ptrTo(lsproto.TextDocumentContentChangePartial{
						Range: lsproto.Range{
							Start: lsproto.Position{
								Line:      0,
								Character: 0,
							},
							End: lsproto.Position{
								Line:      0,
								Character: 0,
							},
						},
						Text: ";",
					}),
				},
			})

			lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
			assert.NilError(t, err)
			programAfter := lsAfter.GetProgram()

			// Unchanged file should be reused
			assert.Equal(t, programAfter.GetSourceFile("/home/projects/TS/p1/src/index.ts"), indexFileBefore)
		})

		t.Run("change can pull in new files", func(t *testing.T) {
			t.Parallel()
			files := maps.Clone(defaultFiles)
			files["/home/projects/TS/p1/y.ts"] = `export const y = 2;`
			session, _ := projecttestutil.Setup(files)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			// Verify y.ts is not initially in the program
			lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			programBefore := lsBefore.GetProgram()
			assert.Check(t, programBefore.GetSourceFile("/home/projects/TS/p1/y.ts") == nil)

			session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{
				{
					Partial: ptrTo(lsproto.TextDocumentContentChangePartial{
						Range: lsproto.Range{
							Start: lsproto.Position{
								Line:      0,
								Character: 0,
							},
							End: lsproto.Position{
								Line:      0,
								Character: 0,
							},
						},
						Text: `import { y } from "../y";\n`,
					}),
				},
			})

			lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			programAfter := lsAfter.GetProgram()

			// y.ts should now be included in the program
			assert.Assert(t, programAfter.GetSourceFile("/home/projects/TS/p1/y.ts") != nil)
		})

		t.Run("single-file change followed by config change reloads program", func(t *testing.T) {
			t.Parallel()
			files := maps.Clone(defaultFiles)
			files["/home/projects/TS/p1/tsconfig.json"] = `{
				"compilerOptions": {
					"noLib": true,
					"module": "nodenext",
					"strict": true
				},
				"include": ["src/index.ts"]
			}`
			session, utils := projecttestutil.Setup(files)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			programBefore := lsBefore.GetProgram()
			assert.Equal(t, len(programBefore.GetSourceFiles()), 2)

			session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{
				{
					Partial: ptrTo(lsproto.TextDocumentContentChangePartial{
						Range: lsproto.Range{
							Start: lsproto.Position{
								Line:      0,
								Character: 0,
							},
							End: lsproto.Position{
								Line:      0,
								Character: 0,
							},
						},
						Text: "\n",
					}),
				},
			})

			err = utils.FS().WriteFile("/home/projects/TS/p1/tsconfig.json", `{
				"compilerOptions": {
					"noLib": true,
					"module": "nodenext",
					"strict": true
				},
				"include": ["./**/*"]
			}`, false)
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeChanged,
					Uri:  "file:///home/projects/TS/p1/tsconfig.json",
				},
			})

			lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			programAfter := lsAfter.GetProgram()
			assert.Equal(t, len(programAfter.GetSourceFiles()), 3)
		})
	})

	t.Run("DidCloseFile", func(t *testing.T) {
		t.Parallel()
		t.Run("Configured projects", func(t *testing.T) {
			t.Parallel()
			t.Run("delete a file, close it, recreate it", func(t *testing.T) {
				t.Parallel()
				files := maps.Clone(defaultFiles)
				session, utils := projecttestutil.Setup(files)

				session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)
				session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

				assert.NilError(t, utils.FS().Remove("/home/projects/TS/p1/src/x.ts"))

				session.DidCloseFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
				ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
				assert.NilError(t, err)
				program := ls.GetProgram()
				assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil)

				err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", "", false)
				assert.NilError(t, err)

				session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, "", lsproto.LanguageKindTypeScript)

				ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
				assert.NilError(t, err)
				program = ls.GetProgram()
				assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil)
				assert.Equal(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "")
			})
		})

		t.Run("Inferred projects", func(t *testing.T) {
			t.Parallel()
			t.Run("delete a file, close it, recreate it", func(t *testing.T) {
				t.Parallel()
				files := maps.Clone(defaultFiles)
				delete(files, "/home/projects/TS/p1/tsconfig.json")
				session, utils := projecttestutil.Setup(files)

				session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)
				session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

				err := utils.FS().Remove("/home/projects/TS/p1/src/x.ts")
				assert.NilError(t, err)

				session.DidCloseFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts")

				ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
				assert.NilError(t, err)
				program := ls.GetProgram()
				assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil)

				err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", "", false)
				assert.NilError(t, err)

				session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, "", lsproto.LanguageKindTypeScript)

				ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
				assert.NilError(t, err)
				program = ls.GetProgram()
				assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil)
				assert.Equal(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "")
			})
		})
	})

	t.Run("DidSaveFile", func(t *testing.T) {
		t.Parallel()
		t.Run("save event first", func(t *testing.T) {
			t.Parallel()
			session, _ := projecttestutil.Setup(defaultFiles)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			snapshot, release := session.Snapshot()
			defer release()
			assert.Equal(t, snapshot.ID(), uint64(1))

			session.DidSaveFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeChanged,
					Uri:  "file:///home/projects/TS/p1/src/index.ts",
				},
			})

			session.WaitForBackgroundTasks()
			snapshot, release = session.Snapshot()
			defer release()
			// We didn't need a snapshot change, but the session overlays should be updated.
			assert.Equal(t, snapshot.ID(), uint64(1))

			// Open another file to force a snapshot update so we can see the changes.
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)
			snapshot, release = session.Snapshot()
			defer release()
			assert.Equal(t, snapshot.GetFile("/home/projects/TS/p1/src/index.ts").MatchesDiskText(), true)
		})

		t.Run("watch event first", func(t *testing.T) {
			t.Parallel()
			session, _ := projecttestutil.Setup(defaultFiles)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			snapshot, release := session.Snapshot()
			defer release()
			assert.Equal(t, snapshot.ID(), uint64(1))

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeChanged,
					Uri:  "file:///home/projects/TS/p1/src/index.ts",
				},
			})
			session.DidSaveFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts")

			session.WaitForBackgroundTasks()
			snapshot, release = session.Snapshot()
			defer release()
			// We didn't need a snapshot change, but the session overlays should be updated.
			assert.Equal(t, snapshot.ID(), uint64(1))

			// Open another file to force a snapshot update so we can see the changes.
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)
			snapshot, release = session.Snapshot()
			defer release()
			assert.Equal(t, snapshot.GetFile("/home/projects/TS/p1/src/index.ts").MatchesDiskText(), true)
		})
	})

	t.Run("Source file sharing", func(t *testing.T) {
		t.Parallel()
		t.Run("projects with similar options share source files", func(t *testing.T) {
			t.Parallel()
			files := maps.Clone(defaultFiles)
			files["/home/projects/TS/p2/tsconfig.json"] = `{
				"compilerOptions": {
					"noLib": true,
					"module": "nodenext",
					"strict": true,
					"noCheck": true
				}
			}`
			files["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";`
			session, _ := projecttestutil.Setup(files)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p2/src/index.ts", 1, files["/home/projects/TS/p2/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			snapshot, release := session.Snapshot()
			defer release()
			assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2)

			ls1, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program1 := ls1.GetProgram()

			ls2, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts")
			assert.NilError(t, err)
			program2 := ls2.GetProgram()

			assert.Equal(t,
				program1.GetSourceFile("/home/projects/TS/p1/src/x.ts"),
				program2.GetSourceFile("/home/projects/TS/p1/src/x.ts"),
			)
		})

		t.Run("projects with different options do not share source files", func(t *testing.T) {
			t.Parallel()
			files := maps.Clone(defaultFiles)
			files["/home/projects/TS/p2/tsconfig.json"] = `{
				"compilerOptions": {
					"module": "nodenext",
					"jsx": "react"
				}
			}`
			files["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";`
			session, _ := projecttestutil.Setup(files)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p2/src/index.ts", 1, files["/home/projects/TS/p2/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			snapshot, release := session.Snapshot()
			defer release()
			assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2)

			ls1, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program1 := ls1.GetProgram()

			ls2, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts")
			assert.NilError(t, err)
			program2 := ls2.GetProgram()

			x1 := program1.GetSourceFile("/home/projects/TS/p1/src/x.ts")
			x2 := program2.GetSourceFile("/home/projects/TS/p1/src/x.ts")
			assert.Assert(t, x1 != nil && x2 != nil)
			assert.Assert(t, x1 != x2)
		})
	})

	t.Run("DidChangeWatchedFiles", func(t *testing.T) {
		t.Parallel()

		t.Run("change open file", func(t *testing.T) {
			t.Parallel()
			files := maps.Clone(defaultFiles)
			session, utils := projecttestutil.Setup(files)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			programBefore := lsBefore.GetProgram()

			err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 2;`, false)
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeChanged,
					Uri:  "file:///home/projects/TS/p1/src/x.ts",
				},
			})

			lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			// Program should remain the same since the file is open and changes are handled through DidChangeTextDocument
			assert.Equal(t, programBefore, lsAfter.GetProgram())
		})

		t.Run("change closed program file", func(t *testing.T) {
			t.Parallel()
			files := maps.Clone(defaultFiles)
			session, utils := projecttestutil.Setup(files)

			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			programBefore := lsBefore.GetProgram()

			err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 2;`, false)
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeChanged,
					Uri:  "file:///home/projects/TS/p1/src/x.ts",
				},
			})

			lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			assert.Check(t, lsAfter.GetProgram() != programBefore)
		})

		t.Run("change program file not in tsconfig root files", func(t *testing.T) {
			t.Parallel()
			for _, workspaceDir := range []string{"/", "/home/projects/TS/p1", "/somewhere/else/entirely"} {
				t.Run("workspaceDir="+strings.ReplaceAll(workspaceDir, "/", "_"), func(t *testing.T) {
					t.Parallel()
					files := map[string]any{
						"/home/projects/TS/p1/tsconfig.json": `{
							"compilerOptions": {
								"noLib": true,
								"module": "nodenext",
								"strict": true
							},
							"files": ["src/index.ts"]
						}`,
						"/home/projects/TS/p1/src/index.ts": `import { x } from "../../x";`,
						"/home/projects/TS/x.ts":            `export const x = 1;`,
					}

					session, utils := projecttestutil.SetupWithOptions(files, &project.SessionOptions{
						CurrentDirectory:   workspaceDir,
						DefaultLibraryPath: bundled.LibPath(),
						TypingsLocation:    projecttestutil.TestTypingsLocation,
						PositionEncoding:   lsproto.PositionEncodingKindUTF8,
						WatchEnabled:       true,
						LoggingEnabled:     true,
					})
					session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
					lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
					assert.NilError(t, err)
					programBefore := lsBefore.GetProgram()
					session.WaitForBackgroundTasks()

					var xWatched bool
				outer:
					for _, call := range utils.Client().WatchFilesCalls() {
						for _, watcher := range call.Watchers {
							if core.Must(glob.Parse(*watcher.GlobPattern.Pattern)).Match("/home/projects/TS/x.ts") {
								xWatched = true
								break outer
							}
						}
					}
					assert.Check(t, xWatched)

					err = utils.FS().WriteFile("/home/projects/TS/x.ts", `export const x = 2;`, false)
					assert.NilError(t, err)

					session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
						{
							Type: lsproto.FileChangeTypeChanged,
							Uri:  "file:///home/projects/TS/x.ts",
						},
					})

					lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
					assert.NilError(t, err)
					assert.Check(t, lsAfter.GetProgram() != programBefore)
				})
			}
		})

		t.Run("change config file", func(t *testing.T) {
			t.Parallel()
			files := map[string]any{
				"/home/projects/TS/p1/tsconfig.json": `{
					"compilerOptions": {
						"noLib": true,
						"strict": false
					}
				}`,
				"/home/projects/TS/p1/src/x.ts": `export declare const x: number | undefined;`,
				"/home/projects/TS/p1/src/index.ts": `
					import { x } from "./x";
					let y: number = x;`,
			}

			session, utils := projecttestutil.Setup(files)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program := ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0)

			err = utils.FS().WriteFile("/home/projects/TS/p1/tsconfig.json", `{
				"compilerOptions": {
					"noLib": false,
					"strict": true
				}
			}`, false)
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeChanged,
					Uri:  "file:///home/projects/TS/p1/tsconfig.json",
				},
			})

			ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program = ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1)
		})

		t.Run("delete explicitly included file", func(t *testing.T) {
			t.Parallel()
			files := map[string]any{
				"/home/projects/TS/p1/tsconfig.json": `{
					"compilerOptions": {
						"noLib": true
					},
					"files": ["src/index.ts", "src/x.ts"]
				}`,
				"/home/projects/TS/p1/src/x.ts":     `export declare const x: number | undefined;`,
				"/home/projects/TS/p1/src/index.ts": `import { x } from "./x";`,
			}
			session, utils := projecttestutil.Setup(files)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program := ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0)

			err = utils.FS().Remove("/home/projects/TS/p1/src/x.ts")
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeDeleted,
					Uri:  "file:///home/projects/TS/p1/src/x.ts",
				},
			})

			ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program = ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1)
			assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil)
		})

		t.Run("delete wildcard included file", func(t *testing.T) {
			t.Parallel()
			files := map[string]any{
				"/home/projects/TS/p1/tsconfig.json": `{
					"compilerOptions": {
						"noLib": true
					},
					"include": ["src"]
				}`,
				"/home/projects/TS/p1/src/index.ts": `let x = 2;`,
				"/home/projects/TS/p1/src/x.ts":     `let y = x;`,
			}
			session, utils := projecttestutil.Setup(files)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)

			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
			assert.NilError(t, err)
			program := ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 0)

			err = utils.FS().Remove("/home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeDeleted,
					Uri:  "file:///home/projects/TS/p1/src/index.ts",
				},
			})

			ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts")
			assert.NilError(t, err)
			program = ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 1)
		})

		t.Run("create explicitly included file", func(t *testing.T) {
			t.Parallel()
			files := map[string]any{
				"/home/projects/TS/p1/tsconfig.json": `{
					"compilerOptions": {
						"noLib": true
					},
					"files": ["src/index.ts", "src/y.ts"]
				}`,
				"/home/projects/TS/p1/src/index.ts": `import { y } from "./y";`,
			}
			session, utils := projecttestutil.Setup(files)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program := ls.GetProgram()

			// Initially should have an error because y.ts is missing
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1)

			// Add the missing file
			err = utils.FS().WriteFile("/home/projects/TS/p1/src/y.ts", `export const y = 1;`, false)
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeCreated,
					Uri:  "file:///home/projects/TS/p1/src/y.ts",
				},
			})

			// Error should be resolved
			ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program = ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0)
			assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/y.ts") != nil)
		})

		t.Run("create failed lookup location", func(t *testing.T) {
			t.Parallel()
			files := map[string]any{
				"/home/projects/TS/p1/tsconfig.json": `{
					"compilerOptions": {
						"noLib": true
					},
					"files": ["src/index.ts"]
				}`,
				"/home/projects/TS/p1/src/index.ts": `import { z } from "./z";`,
			}
			session, utils := projecttestutil.Setup(files)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program := ls.GetProgram()

			// Initially should have an error because z.ts is missing
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1)

			// Add a new file through failed lookup watch
			err = utils.FS().WriteFile("/home/projects/TS/p1/src/z.ts", `export const z = 1;`, false)
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeCreated,
					Uri:  "file:///home/projects/TS/p1/src/z.ts",
				},
			})

			// Error should be resolved and the new file should be included in the program
			ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program = ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0)
			assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/z.ts") != nil)
		})

		t.Run("create wildcard included file", func(t *testing.T) {
			t.Parallel()
			files := map[string]any{
				"/home/projects/TS/p1/tsconfig.json": `{
					"compilerOptions": {
						"noLib": true
					},
					"include": ["src"]
				}`,
				"/home/projects/TS/p1/src/index.ts": `a;`,
			}
			session, utils := projecttestutil.Setup(files)
			session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)

			ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program := ls.GetProgram()

			// Initially should have an error because declaration for 'a' is missing
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1)

			// Add a new file through wildcard watch
			err = utils.FS().WriteFile("/home/projects/TS/p1/src/a.ts", `const a = 1;`, false)
			assert.NilError(t, err)

			session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
				{
					Type: lsproto.FileChangeTypeCreated,
					Uri:  "file:///home/projects/TS/p1/src/a.ts",
				},
			})

			// Error should be resolved and the new file should be included in the program
			ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
			assert.NilError(t, err)
			program = ls.GetProgram()
			assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0)
			assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/a.ts") != nil)
		})
	})
}

func ptrTo[T any](v T) *T {
	return &v
}
