package autoimport

import (
	"reflect"
	"testing"

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

func TestWordIndices(t *testing.T) {
	t.Parallel()
	tests := []struct {
		input         string
		expectedWords []string
	}{
		// Basic camelCase
		{
			input:         "camelCase",
			expectedWords: []string{"camelCase", "Case"},
		},
		// snake_case
		{
			input:         "snake_case",
			expectedWords: []string{"snake_case", "case"},
		},
		// ParseURL - uppercase sequence followed by lowercase
		{
			input:         "ParseURL",
			expectedWords: []string{"ParseURL", "URL"},
		},
		// XMLHttpRequest - multiple uppercase sequences
		{
			input:         "XMLHttpRequest",
			expectedWords: []string{"XMLHttpRequest", "HttpRequest", "Request"},
		},
		// Single word lowercase
		{
			input:         "hello",
			expectedWords: []string{"hello"},
		},
		// Single word uppercase
		{
			input:         "HELLO",
			expectedWords: []string{"HELLO"},
		},
		// Mixed with numbers
		{
			input:         "parseHTML5Parser",
			expectedWords: []string{"parseHTML5Parser", "HTML5Parser", "Parser"},
		},
		// Underscore variations
		{
			input:         "__proto__",
			expectedWords: []string{"__proto__", "proto__"},
		},
		{
			input:         "_private_member",
			expectedWords: []string{"_private_member", "member"},
		},
		// Single character
		{
			input:         "a",
			expectedWords: []string{"a"},
		},
		{
			input:         "A",
			expectedWords: []string{"A"},
		},
		// Consecutive underscores
		{
			input:         "test__double__underscore",
			expectedWords: []string{"test__double__underscore", "double__underscore", "underscore"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			t.Parallel()
			indices := wordIndices(tt.input)

			// Convert indices to actual word slices for comparison
			var actualWords []string
			for _, idx := range indices {
				actualWords = append(actualWords, tt.input[idx:])
			}

			if !reflect.DeepEqual(actualWords, tt.expectedWords) {
				t.Errorf("wordIndices(%q) produced words %v, want %v", tt.input, actualWords, tt.expectedWords)
			}
		})
	}
}

// TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks tests that toRealpath correctly
// follows symlinks for files outside the package directory (e.g. node_modules entries).
// Without this, the module resolver uses unresolved symlink paths as cache keys, causing
// the same file to be loaded multiple times and triggering massive memory usage when
// barrel files re-export from many symlinked packages (issue #2780).
func TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks(t *testing.T) {
	t.Parallel()

	// Simulate a layout where the package directory is itself a symlink (e.g. Bazel's
	// convenience symlinks or pnpm's virtual store):
	//   /symlink-bin/pkg/              -> symlink to /real/bin/pkg/
	//   /real/bin/pkg/node_modules/dep -> symlink to /real/dep/
	//
	// When toRealpath is used as the module resolver's Realpath, it must follow
	// the node_modules symlink so that /real/bin/pkg/node_modules/dep/index.d.ts
	// resolves to /real/dep/index.d.ts — otherwise the same dep file gets different
	// cache keys depending on which path it was reached through.
	fs := vfstest.FromMap(map[string]any{
		"/symlink-bin/pkg":                vfstest.Symlink("/real/bin/pkg"),
		"/real/bin/pkg/index.d.ts":        "export declare const a: number;",
		"/real/bin/pkg/node_modules/dep":  vfstest.Symlink("/real/dep"),
		"/real/dep/index.d.ts":            "export declare const b: number;",
		"/real/dep/src/utils/helper.d.ts": "export declare const c: number;",
	}, true)

	toRealpath, _ := getPackageRealpathFuncs(fs, "/symlink-bin/pkg")

	// Files inside the package should be converted via string replacement (fast path).
	assert.Equal(
		t,
		toRealpath("/symlink-bin/pkg/index.d.ts"),
		"/real/bin/pkg/index.d.ts",
		"package files should be converted via prefix replacement",
	)

	// Files outside the package (e.g. node_modules symlinks) should be resolved via
	// fs.Realpath so the cache key is the canonical realpath, not the symlink path.
	assert.Equal(
		t,
		toRealpath("/real/bin/pkg/node_modules/dep/index.d.ts"),
		"/real/dep/index.d.ts",
		"node_modules symlinks must be followed so the same file gets a consistent cache key",
	)

	// Files in subdirectories of an already-resolved external package should
	// use the cached prefix mapping without additional realpath calls.
	assert.Equal(
		t,
		toRealpath("/real/bin/pkg/node_modules/dep/src/utils/helper.d.ts"),
		"/real/dep/src/utils/helper.d.ts",
		"subdirectories of a resolved external package should use cached prefix mapping",
	)
}

// TestGetPackageRealpathFuncs_DuplicateCacheKeys demonstrates how the broken toRealpath
// causes the same physical file to get different cache keys when reached through different
// symlink paths. In pnpm/Bazel monorepos, multiple packages may have node_modules symlinks
// that point to the same physical dependency. Because toRealpath doesn't follow symlinks
// for files outside the package directory, each path is treated as distinct, leading to
// duplicate file loads and memory bloat (issue #2780).
func TestGetPackageRealpathFuncs_DuplicateCacheKeys(t *testing.T) {
	t.Parallel()

	// Simulate two packages (app-a, app-b) that each have a node_modules symlink to
	// the same shared dependency. This is a typical pnpm/Bazel layout:
	//   /workspace/packages/app-a/              -> symlink to /store/app-a/
	//   /workspace/packages/app-b/              -> symlink to /store/app-b/
	//   /store/app-a/node_modules/shared-lib    -> symlink to /store/shared-lib/
	//   /store/app-b/node_modules/shared-lib    -> symlink to /store/shared-lib/
	fs := vfstest.FromMap(map[string]any{
		"/workspace/packages/app-a":            vfstest.Symlink("/store/app-a"),
		"/workspace/packages/app-b":            vfstest.Symlink("/store/app-b"),
		"/store/app-a/index.d.ts":              "export declare const a: number;",
		"/store/app-b/index.d.ts":              "export declare const b: number;",
		"/store/app-a/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
		"/store/app-b/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
		"/store/shared-lib/index.d.ts":         "export declare const shared: string;",
	}, true)

	toRealpathA, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-a")
	toRealpathB, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-b")

	sharedFileViaA := "/store/app-a/node_modules/shared-lib/index.d.ts"
	sharedFileViaB := "/store/app-b/node_modules/shared-lib/index.d.ts"

	resolvedA := toRealpathA(sharedFileViaA)
	resolvedB := toRealpathB(sharedFileViaB)

	// Both should resolve to the same canonical realpath so the module resolver
	// uses a single cache key for the shared dependency, avoiding duplicate loads.
	expectedRealpath := "/store/shared-lib/index.d.ts"
	assert.Equal(t, resolvedA, expectedRealpath,
		"app-a's toRealpath should follow the node_modules symlink to the realpath")
	assert.Equal(t, resolvedB, expectedRealpath,
		"app-b's toRealpath should follow the node_modules symlink to the realpath")
}

// TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps tests that even when the
// package directory itself is NOT a symlink, toRealpath still follows symlinks for files
// outside the package (e.g. re-exports reaching into symlinked node_modules dependencies).
func TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps(t *testing.T) {
	t.Parallel()

	fs := vfstest.FromMap(map[string]any{
		"/real/my-pkg/index.d.ts":       "export declare const a: number;",
		"/real/my-pkg/node_modules/dep": vfstest.Symlink("/real/dep"),
		"/real/dep/index.d.ts":          "export declare const b: number;",
	}, true)

	toRealpath, _ := getPackageRealpathFuncs(fs, "/real/my-pkg")

	// Files inside the (non-symlinked) package should be returned unchanged.
	assert.Equal(
		t,
		toRealpath("/real/my-pkg/index.d.ts"),
		"/real/my-pkg/index.d.ts",
	)

	// Files outside the package reached via symlinked node_modules should still be resolved.
	assert.Equal(
		t,
		toRealpath("/real/my-pkg/node_modules/dep/index.d.ts"),
		"/real/dep/index.d.ts",
		"symlinked deps must be resolved even when the package dir itself is not a symlink",
	)
}
