package tsctests

import (
	"fmt"
	"strings"
	"testing"

	"github.com/microsoft/typescript-go/internal/vfs/vfstest"
)

func TestWatch(t *testing.T) {
	t.Parallel()
	bunDependencyTest := func() *tscInput {
		files := FileMap{}
		var index strings.Builder
		var fileNames strings.Builder
		fileNames.WriteString(`"index.ts"`)
		for i := range 12 {
			name := fmt.Sprintf("pkg%d", i)
			value := fmt.Sprintf("value%d", i)
			index.WriteString(fmt.Sprintf(`import { %[1]s } from "./node_modules/.bun/%[2]s/index"; %[1]s;`, value, name))
			index.WriteString("\n")
			files["/home/src/workspaces/project/node_modules/.bun/"+name+"/index.ts"] = fmt.Sprintf("export const %s = %d;", value, i)
			fileNames.WriteString(fmt.Sprintf(`, "node_modules/.bun/%s/index.ts"`, name))
		}
		files["/home/src/workspaces/project/index.ts"] = index.String()
		files["/home/src/workspaces/project/tsconfig.json"] = fmt.Sprintf(`{
	"compilerOptions": {},
	"files": [%s]
}`, fileNames.String())
		return &tscInput{
			subScenario:     "watch handles many bun dependency files",
			files:           files,
			commandLineArgs: []string{"--watch"},
		}
	}
	testCases := []*tscInput{
		{
			subScenario: "watch with no tsconfig",
			files: FileMap{
				"/home/src/workspaces/project/index.ts": "",
			},
			commandLineArgs: []string{"index.ts", "--watch"},
		},
		{
			subScenario: "watch with tsconfig and incremental",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      "",
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch", "--incremental"},
		},
		bunDependencyTest(),
		{
			subScenario: "watch skips build when no files change",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `const x: number = 1;`,
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				noChange,
			},
		},
		{
			subScenario: "watch rebuilds when file is modified",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `const x: number = 1;`,
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("modify file", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/index.ts", `const x: number = 2;`)
				}),
			},
		},
		{
			subScenario: "watch rebuilds when source file is deleted",
			files: FileMap{
				"/home/src/workspaces/project/a.ts":          `import { b } from "./b";`,
				"/home/src/workspaces/project/b.ts":          `export const b = 1;`,
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption: "delete imported file",
					edit: func(sys *TestSys) {
						sys.removeNoError("/home/src/workspaces/project/b.ts")
					},
					expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)",
				},
			},
		},
		{
			subScenario: "watch detects new file resolving failed import",
			files: FileMap{
				"/home/src/workspaces/project/a.ts":          `import { b } from "./b";`,
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("create missing file", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/b.ts", `export const b = 1;`)
				}),
			},
		},
		// Directory-level change detection via imports
		{
			subScenario: "watch detects imported file added in new directory",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { util } from "./lib/util";`,
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("create directory and imported file", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/lib/util.ts", `export const util = "hello";`)
				}),
			},
		},
		{
			subScenario: "watch detects imported directory removed",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { util } from "./lib/util";`,
				"/home/src/workspaces/project/lib/util.ts":   `export const util = "hello";`,
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption: "remove directory with imported file",
					edit: func(sys *TestSys) {
						sys.removeNoError("/home/src/workspaces/project/lib/util.ts")
					},
					expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)",
				},
			},
		},
		{
			subScenario: "watch detects import path restructured",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { util } from "./lib/util";`,
				"/home/src/workspaces/project/lib/util.ts":   `export const util = "v1";`,
				"/home/src/workspaces/project/tsconfig.json": "{}",
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("move file to new path and update import", func(sys *TestSys) {
					sys.removeNoError("/home/src/workspaces/project/lib/util.ts")
					sys.writeFileNoError("/home/src/workspaces/project/src/util.ts", `export const util = "v2";`)
					sys.writeFileNoError("/home/src/workspaces/project/index.ts", `import { util } from "./src/util";`)
				}),
			},
		},
		// tsconfig include/exclude change detection
		{
			subScenario: "watch rebuilds when tsconfig include pattern adds file",
			files: FileMap{
				"/home/src/workspaces/project/index.ts": `const x = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"compilerOptions": {},
	"include": ["*.ts"]
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("widen include pattern to add src dir", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/src/extra.ts", `export const extra = 2;`)
					sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{
	"compilerOptions": {},
	"include": ["*.ts", "src/**/*.ts"]
}`)
				}),
			},
		},
		{
			subScenario: "watch rebuilds when tsconfig is modified to change strict",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `const x = null; const y: string = x;`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("enable strict mode", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{"compilerOptions": {"strict": true}}`)
				}),
			},
		},
		// Path resolution: tsconfig include pointing to non-existent directory
		{
			subScenario: "watch detects file added to previously non-existent include path",
			files: FileMap{
				"/home/src/workspaces/project/index.ts": `const x = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"compilerOptions": {},
	"include": ["index.ts", "src/**/*.ts"]
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("create src dir with ts file matching include", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`)
				}),
			},
		},
		{
			subScenario: "watch detects new file in existing include directory",
			files: FileMap{
				"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"compilerOptions": {},
	"include": ["src/**/*.ts"]
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("add new file to existing src directory", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/src/b.ts", `export const b = 2;`)
				}),
			},
		},
		// Wildcard include: nested subdirectory detection
		{
			subScenario: "watch detects file added in new nested subdirectory",
			files: FileMap{
				"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"compilerOptions": {},
	"include": ["src/**/*.ts"]
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("create nested dir with ts file", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/src/deep/nested/util.ts", `export const util = "nested";`)
				}),
			},
		},
		{
			subScenario: "watch detects file added in multiple new subdirectories simultaneously",
			files: FileMap{
				"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"compilerOptions": {},
	"include": ["src/**/*.ts"]
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("create multiple new subdirs with files", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/src/models/user.ts", `export interface User { name: string; }`)
					sys.writeFileNoError("/home/src/workspaces/project/src/utils/format.ts", `export function format(s: string): string { return s.trim(); }`)
				}),
			},
		},
		{
			subScenario: "watch detects nested subdirectory removed and recreated",
			files: FileMap{
				"/home/src/workspaces/project/src/lib/helper.ts": `export const helper = "v1";`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"compilerOptions": {},
	"include": ["src/**/*.ts"]
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption:      "remove nested dir",
					expectedDiff: "incremental has prior state and does not report no-inputs error",
					edit: func(sys *TestSys) {
						sys.removeNoError("/home/src/workspaces/project/src/lib/helper.ts")
					},
				},
				newTscEdit("recreate nested dir with new content", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/src/lib/helper.ts", `export const helper = "v2";`)
				}),
			},
		},
		// Path resolution: import from non-existent node_modules package
		{
			subScenario: "watch detects node modules package added",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { lib } from "mylib";`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("install package in node_modules", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`)
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.js", `exports.lib = "hello";`)
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts", `export declare const lib: string;`)
				}),
			},
		},
		// Path resolution: node_modules package removed
		{
			subScenario: "watch detects node modules package removed",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":                        `import { lib } from "mylib";`,
				"/home/src/workspaces/project/tsconfig.json":                   `{}`,
				"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`,
				"/home/src/workspaces/project/node_modules/mylib/index.js":     `exports.lib = "hello";`,
				"/home/src/workspaces/project/node_modules/mylib/index.d.ts":   `export declare const lib: string;`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption: "remove node_modules package",
					edit: func(sys *TestSys) {
						sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts")
						sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.js")
						sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/package.json")
					},
				},
			},
		},
		// Path resolution: node_modules removed then reinstalled (npm ci after rm -rf)
		{
			subScenario: "watch detects node modules reinstalled after deletion",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":                        `import { lib } from "mylib";`,
				"/home/src/workspaces/project/tsconfig.json":                   `{}`,
				"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`,
				"/home/src/workspaces/project/node_modules/mylib/index.js":     `exports.lib = "hello";`,
				"/home/src/workspaces/project/node_modules/mylib/index.d.ts":   `export declare const lib: string;`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption: "delete node_modules entirely",
					edit: func(sys *TestSys) {
						sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts")
						sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.js")
						sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/package.json")
					},
				},
				newTscEdit("reinstall node_modules", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`)
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.js", `exports.lib = "hello";`)
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts", `export declare const lib: string;`)
				}),
			},
		},
		// Config file lifecycle
		{
			subScenario: "watch handles tsconfig deleted",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `const x = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption:      "delete tsconfig",
					expectedDiff: "incremental reports config read error while clean build without tsconfig prints usage help",
					edit: func(sys *TestSys) {
						sys.removeNoError("/home/src/workspaces/project/tsconfig.json")
					},
				},
			},
		},
		{
			subScenario: "watch handles tsconfig with extends base modified",
			files: FileMap{
				"/home/src/workspaces/project/index.ts": `const x = null; const y: string = x;`,
				"/home/src/workspaces/project/base.json": `{
	"compilerOptions": { "strict": false }
}`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"extends": "./base.json"
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("modify base config to enable strict", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/base.json", `{
	"compilerOptions": { "strict": true }
}`)
				}),
			},
		},
		{
			subScenario: "watch rebuilds when tsconfig is touched but content unchanged",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `const x = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("touch tsconfig without changing content", func(sys *TestSys) {
					content := sys.readFileNoError("/home/src/workspaces/project/tsconfig.json")
					sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", content)
				}),
			},
		},
		{
			subScenario: "watch with tsconfig files list entry deleted",
			files: FileMap{
				"/home/src/workspaces/project/a.ts": `export const a = 1;`,
				"/home/src/workspaces/project/b.ts": `export const b = 2;`,
				"/home/src/workspaces/project/tsconfig.json": `{
	"compilerOptions": {},
	"files": ["a.ts", "b.ts"]
}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("delete file listed in files array", func(sys *TestSys) {
					sys.removeNoError("/home/src/workspaces/project/b.ts")
				}),
			},
		},
		// Module resolution & dependencies
		{
			subScenario: "watch detects module going missing then coming back",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { util } from "./util";`,
				"/home/src/workspaces/project/util.ts":       `export const util = "v1";`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption: "delete util module",
					edit: func(sys *TestSys) {
						sys.removeNoError("/home/src/workspaces/project/util.ts")
					},
					expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module",
				},
				newTscEdit("recreate util module with new content", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/util.ts", `export const util = "v2";`)
				}),
			},
		},
		{
			subScenario: "watch detects scoped package installed",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { lib } from "@scope/mylib";`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("install scoped package", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/package.json", `{"name": "@scope/mylib", "types": "index.d.ts"}`)
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/index.d.ts", `export declare const lib: string;`)
				}),
			},
		},
		{
			subScenario: "watch detects package json types field edited",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":                        `import { lib } from "mylib";`,
				"/home/src/workspaces/project/tsconfig.json":                   `{}`,
				"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "types": "old.d.ts"}`,
				"/home/src/workspaces/project/node_modules/mylib/old.d.ts":     `export declare const lib: number;`,
				"/home/src/workspaces/project/node_modules/mylib/new.d.ts":     `export declare const lib: string;`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("change package.json types field", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "types": "new.d.ts"}`)
				}),
			},
		},
		{
			subScenario: "watch detects at-types package installed later",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":                          `import * as lib from "untyped-lib";`,
				"/home/src/workspaces/project/tsconfig.json":                     `{}`,
				"/home/src/workspaces/project/node_modules/untyped-lib/index.js": `module.exports = {};`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("install @types for the library", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/index.d.ts", `declare module "untyped-lib" { export const value: string; }`)
					sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/package.json", `{"name": "@types/untyped-lib", "types": "index.d.ts"}`)
				}),
			},
		},
		// File operations
		{
			subScenario: "watch detects file renamed and renamed back",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { helper } from "./helper";`,
				"/home/src/workspaces/project/helper.ts":     `export const helper = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				{
					caption: "rename helper to helper2",
					edit: func(sys *TestSys) {
						sys.renameFileNoError("/home/src/workspaces/project/helper.ts", "/home/src/workspaces/project/helper2.ts")
					},
					expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module",
				},
				newTscEdit("rename back to helper", func(sys *TestSys) {
					sys.renameFileNoError("/home/src/workspaces/project/helper2.ts", "/home/src/workspaces/project/helper.ts")
				}),
			},
		},
		{
			subScenario: "watch detects file deleted and new file added simultaneously",
			files: FileMap{
				"/home/src/workspaces/project/a.ts":          `import { b } from "./b";`,
				"/home/src/workspaces/project/b.ts":          `export const b = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("delete b.ts and create c.ts with updated import", func(sys *TestSys) {
					sys.removeNoError("/home/src/workspaces/project/b.ts")
					sys.writeFileNoError("/home/src/workspaces/project/c.ts", `export const c = 2;`)
					sys.writeFileNoError("/home/src/workspaces/project/a.ts", `import { c } from "./c";`)
				}),
			},
		},
		{
			subScenario: "watch handles file rapidly recreated",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `import { val } from "./data";`,
				"/home/src/workspaces/project/data.ts":       `export const val = "original";`,
				"/home/src/workspaces/project/tsconfig.json": `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("delete and immediately recreate with new content", func(sys *TestSys) {
					sys.removeNoError("/home/src/workspaces/project/data.ts")
					sys.writeFileNoError("/home/src/workspaces/project/data.ts", `export const val = "recreated";`)
				}),
			},
		},
		// Symlinks — only node_modules symlinks are resolved via Realpath,
		// matching the TypeScript compiler's behavior (see program.ts:2119).
		{
			subScenario: "watch detects change in symlinked node_modules file",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":                     `import { shared } from "shared";`,
				"/home/src/workspaces/shared/index.ts":                      `export const shared = "v1";`,
				"/home/src/workspaces/project/node_modules/shared/index.ts": vfstest.Symlink("/home/src/workspaces/shared/index.ts"),
				"/home/src/workspaces/project/tsconfig.json":                `{}`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("modify symlink target", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/shared/index.ts", `export const shared = "v2";`)
				}),
			},
		},
		// Ancestor fallback stability — when a tsconfig include references a
		// directory that doesn't exist
		{
			subScenario: "watch stability with ancestor directory fallback",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `const x: number = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{ "include": ["*.ts", "missing/**/*"] }`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("trivial file change", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/index.ts", `const x: number = 2;`)
				}),
			},
		},
		// Ancestor fallback: creating deeply nested directories that didn't
		// exist at initial build time should trigger a rebuild and re-watch.
		{
			subScenario: "watch detects file added in deeply nested non-existent include path",
			files: FileMap{
				"/home/src/workspaces/project/index.ts":      `const x: number = 1;`,
				"/home/src/workspaces/project/tsconfig.json": `{ "include": ["*.ts", "deep/nested/dir/**/*"] }`,
			},
			commandLineArgs: []string{"--watch"},
			edits: []*tscEdit{
				newTscEdit("create deeply nested file matching include", func(sys *TestSys) {
					sys.writeFileNoError("/home/src/workspaces/project/deep/nested/dir/added.ts", `export const added = 1;`)
				}),
			},
		},
	}

	for _, test := range testCases {
		test.run(t, "commandLineWatch")
	}
}

func listToTsconfig(base string, tsconfigOpts ...string) (string, string) {
	optionString := strings.Join(tsconfigOpts, ",\n            ")
	tsconfigText := `{
	"compilerOptions": {
`
	after := "            "
	if base != "" {
		tsconfigText += "            " + base
		after = ",\n            "
	}
	if len(tsconfigOpts) != 0 {
		tsconfigText += after + optionString
	}
	tsconfigText += `
	}
}`
	return tsconfigText, optionString
}

func toTsconfig(base string, compilerOpts string) string {
	tsconfigText, _ := listToTsconfig(base, compilerOpts)
	return tsconfigText
}

func noEmitWatchTestInput(
	subScenario string,
	commandLineArgs []string,
	aText string,
	tsconfigOptions []string,
) *tscInput {
	noEmitOpt := `"noEmit": true`
	tsconfigText, optionString := listToTsconfig(noEmitOpt, tsconfigOptions...)
	return &tscInput{
		subScenario:     subScenario,
		commandLineArgs: commandLineArgs,
		files: FileMap{
			"/home/src/workspaces/project/a.ts":          aText,
			"/home/src/workspaces/project/tsconfig.json": tsconfigText,
		},
		edits: []*tscEdit{
			newTscEdit("fix error", func(sys *TestSys) {
				sys.writeFileNoError("/home/src/workspaces/project/a.ts", `const a = "hello";`)
			}),
			newTscEdit("emit after fixing error", func(sys *TestSys) {
				sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig("", optionString))
			}),
			newTscEdit("no emit run after fixing error", func(sys *TestSys) {
				sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig(noEmitOpt, optionString))
			}),
			newTscEdit("introduce error", func(sys *TestSys) {
				sys.writeFileNoError("/home/src/workspaces/project/a.ts", aText)
			}),
			newTscEdit("emit when error", func(sys *TestSys) {
				sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig("", optionString))
			}),
			newTscEdit("no emit run when error", func(sys *TestSys) {
				sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig(noEmitOpt, optionString))
			}),
		},
	}
}

func newTscEdit(name string, edit func(sys *TestSys)) *tscEdit {
	return &tscEdit{caption: name, edit: edit}
}

func TestTscNoEmitWatch(t *testing.T) {
	t.Parallel()

	testCases := []*tscInput{
		noEmitWatchTestInput(
			"syntax errors",
			[]string{"-w"},
			`const a = "hello`,
			nil,
		),
		noEmitWatchTestInput(
			"semantic errors",
			[]string{"-w"},
			`const a: number = "hello"`,
			nil,
		),
		noEmitWatchTestInput(
			"dts errors without dts enabled",
			[]string{"-w"},
			`const a = class { private p = 10; };`,
			nil,
		),
		noEmitWatchTestInput(
			"dts errors",
			[]string{"-w"},
			`const a = class { private p = 10; };`,
			[]string{`"declaration": true`},
		),
	}

	for _, test := range testCases {
		test.run(t, "noEmit")
	}
}
