package project_test

import (
	"context"
	"testing"

	"github.com/microsoft/typescript-go/internal/bundled"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
	"gotest.tools/v3/assert"
)

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

	files := map[string]any{
		"/src/tsconfig.json":     `{"compilerOptions": {"strict": false}}`,
		"/src/tsconfig.all.json": `{"compilerOptions": {"strict": true}}`,
		"/src/index.ts":          `export const x = 1;`,
	}
	uri := lsproto.DocumentUri("file:///src/index.ts")

	t.Run("picks up custom config and switches on preference change", func(t *testing.T) {
		t.Parallel()
		session, _ := projecttestutil.Setup(files)

		session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
		ls, err := session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)

		snapshot := session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json")
		assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse)

		prefs := lsutil.NewDefaultUserPreferences()
		prefs.CustomConfigFileName = "tsconfig.all.json"
		session.Configure(prefs)

		ls, err = session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)

		snapshot = session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.all.json")
		assert.Equal(t, ls.GetProgram().Options().Strict, core.TSTrue)
	})

	t.Run("uses tsconfig.json when customConfigFileName is empty", func(t *testing.T) {
		t.Parallel()
		session, _ := projecttestutil.Setup(files)

		prefs := lsutil.NewDefaultUserPreferences()
		// default for CustomConfigFileName is "".
		assert.Equal(t, prefs.CustomConfigFileName, "")
		session.Configure(prefs)

		session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
		_, err := session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)

		snapshot := session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json")
	})

	t.Run("falls back to tsconfig.json when custom config missing", func(t *testing.T) {
		t.Parallel()
		session, _ := projecttestutil.Setup(files)

		prefs := lsutil.NewDefaultUserPreferences()
		prefs.CustomConfigFileName = "tsconfig.nonexistent.json"
		session.Configure(prefs)

		session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
		_, err := session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)

		snapshot := session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json")
	})

	t.Run("reverts to tsconfig.json when custom config preference is cleared", func(t *testing.T) {
		t.Parallel()
		session, _ := projecttestutil.Setup(files)

		// Step 1: Open file, verify it uses tsconfig.json (strict: false)
		session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
		ls, err := session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)

		snapshot := session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json")
		assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse)

		// Step 2: Switch to custom config (strict: true)
		prefs := lsutil.NewDefaultUserPreferences()
		prefs.CustomConfigFileName = "tsconfig.all.json"
		session.Configure(prefs)

		ls, err = session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)

		snapshot = session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.all.json")
		assert.Equal(t, ls.GetProgram().Options().Strict, core.TSTrue)

		// Step 3: Clear custom config preference, should revert to tsconfig.json (strict: false)
		prefs = lsutil.NewDefaultUserPreferences()
		prefs.CustomConfigFileName = ""
		session.Configure(prefs)

		ls, err = session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)

		snapshot = session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json")
		assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse)
	})

	// This test demonstrates the bug reported in #2020: after changing
	// customConfigFileName, the server does not schedule a diagnostics refresh,
	// so the VS Code client never knows to re-pull diagnostics and shows stale results.
	t.Run("schedules diagnostics refresh when custom config preference changes", func(t *testing.T) {
		t.Parallel()
		session, utils := projecttestutil.Setup(files)

		session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
		_, err := session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)
		session.WaitForBackgroundTasks()

		// Record baseline refresh call count
		baselineRefreshCount := len(utils.Client().RefreshDiagnosticsCalls())

		// Change the custom config preference
		prefs := lsutil.NewDefaultUserPreferences()
		prefs.CustomConfigFileName = "tsconfig.all.json"
		session.Configure(prefs)

		// GetLanguageService triggers the snapshot update with the new config
		_, err = session.GetLanguageService(context.Background(), uri)
		assert.NilError(t, err)
		session.WaitForBackgroundTasks()

		// The server should have scheduled a diagnostics refresh to tell the client
		// to re-pull diagnostics with the new project configuration.
		refreshCount := len(utils.Client().RefreshDiagnosticsCalls())
		assert.Assert(t, refreshCount > baselineRefreshCount,
			"expected RefreshDiagnostics to be called after customConfigFileName change, got %d calls (baseline %d)",
			refreshCount, baselineRefreshCount)
	})

	t.Run("rejects path traversal in customConfigFileName", func(t *testing.T) {
		t.Parallel()
		for _, invalidName := range []string{
			"/etc/passwd",
			"../tsconfig.json",
			"configs/tsconfig.all.json",
			"..\\tsconfig.json",
			"sub\\dir\\tsconfig.json",
			"..",
			".",
		} {
			prefs := lsutil.ParseUserPreferences(map[string]any{
				"js/ts": map[string]any{
					"customConfigFileName": invalidName,
				},
			})
			assert.Equal(t, prefs.CustomConfigFileName, "",
				"expected customConfigFileName to be cleared for invalid value %q", invalidName)
		}
	})

	t.Run("accepts plain base file names in customConfigFileName", func(t *testing.T) {
		t.Parallel()
		for _, validName := range []string{
			"tsconfig.all.json",
			"tsconfig.editor.json",
			"jsconfig.custom.json",
		} {
			prefs := lsutil.ParseUserPreferences(map[string]any{
				"js/ts": map[string]any{
					"customConfigFileName": validName,
				},
			})
			assert.Equal(t, prefs.CustomConfigFileName, validName,
				"expected customConfigFileName to be %q", validName)
		}
	})

	t.Run("cleans up inferred project when custom config covers file", func(t *testing.T) {
		t.Parallel()

		// Start without any tsconfig.json so file goes into inferred project, then
		// add a custom config that covers the file and verify it moves out of the
		// inferred project (not just getting a new default, but actually cleaned up).
		filesNoConfig := map[string]any{
			"/src/tsconfig.all.json": `{"compilerOptions": {"strict": true}, "include": ["./**/*"]}`,
			"/src/index.ts":          `export const x = 1;`,
		}
		uriLocal := lsproto.DocumentUri("file:///src/index.ts")
		session, _ := projecttestutil.Setup(filesNoConfig)

		session.DidOpenFile(context.Background(), uriLocal, 1, filesNoConfig["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
		_, err := session.GetLanguageService(context.Background(), uriLocal)
		assert.NilError(t, err)

		// Without any config, the file should be in the inferred project only.
		snapshot := session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name(), "/dev/null/inferred")
		projects := snapshot.GetProjectsContainingFile(uriLocal)
		assert.Equal(t, len(projects), 1, "expected file to be in exactly 1 project before config change, got %d", len(projects))

		// Now set custom config to pick up tsconfig.all.json
		prefs := lsutil.NewDefaultUserPreferences()
		prefs.CustomConfigFileName = "tsconfig.all.json"
		session.Configure(prefs)

		_, err = session.GetLanguageService(context.Background(), uriLocal)
		assert.NilError(t, err)

		// File should now be in the configured project only, not duplicated in inferred.
		snapshot = session.Snapshot()
		assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name(), "/src/tsconfig.all.json")
		projects = snapshot.GetProjectsContainingFile(uriLocal)
		assert.Equal(t, len(projects), 1, "expected file to be in exactly 1 project after config change, got %d", len(projects))
	})
}
