package templates

import (
	"context"
	"math/rand"
	"path/filepath"
	"testing"

	"dawn.googlesource.com/dawn/tools/src/cmd/gen/common"
	"dawn.googlesource.com/dawn/tools/src/container"
	"dawn.googlesource.com/dawn/tools/src/fileutils"
	"dawn.googlesource.com/dawn/tools/src/oswrapper"
	"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/sem"
	"github.com/stretchr/testify/require"
)

func setupRunFileTest(t *testing.T) (oswrapper.FSTestOSWrapper, *common.Config, string) {
	t.Helper()

	// Identify the "real" DawnRoot so the mock FS can be populated such that
	// fileutils.DawnRoot(mockFS) returns it.
	realOS := oswrapper.GetRealOSWrapper()
	realDawnRoot := fileutils.DawnRoot(realOS)
	require.NotEmpty(t, realDawnRoot, "Could not determine real DawnRoot")

	osw := oswrapper.CreateFSTestOSWrapper()
	cfg := &common.Config{
		OsWrapper: osw,
	}

	// Populate mock FS with DEPS at realDawnRoot so DawnRoot(osw) works.
	err := osw.MkdirAll(realDawnRoot, 0755)
	require.NoError(t, err, "Failed to create root in mock FS")
	err = osw.WriteFile(filepath.Join(realDawnRoot, "DEPS"), []byte(""), 0644)
	require.NoError(t, err, "Failed to write DEPS")

	// Create the directory structure corresponding to ThisDir() to ensure path lookup works.
	thisDir := fileutils.ThisDir()
	err = osw.MkdirAll(thisDir, 0755)
	require.NoError(t, err, "Failed to create ThisDir in mock FS")

	return osw, cfg, realDawnRoot
}

func createTemplateFile(t *testing.T, osw oswrapper.FilesystemWriter, path, content string) {
	t.Helper()
	err := osw.MkdirAll(filepath.Dir(path), 0755)
	require.NoError(t, err, "Failed to create directory for template")
	err = osw.WriteFile(path, []byte(content), 0644)
	require.NoError(t, err, "Failed to write template file")
}

func TestCmd_Run_FileDiscovery(t *testing.T) {
	osw, cfg, realDawnRoot := setupRunFileTest(t)
	ctx := context.Background()

	// Create the template files
	// Note: The glob pattern in templates.go is "src/tint/**.tmpl" and "test/tint/**.tmpl"
	tmplPath := filepath.Join(realDawnRoot, "src", "tint", "test.tmpl")
	createTemplateFile(t, osw, tmplPath, `Test Template 1`)
	tmplPath2 := filepath.Join(realDawnRoot, "test", "tint", "subdir", "test2.tmpl")
	createTemplateFile(t, osw, tmplPath2, `Test Template 2`)

	c := &CmdSources{}
	err := c.Run(ctx, cfg)
	require.NoError(t, err, "Run sources failed")

	c2 := &CmdTests{}
	err = c2.Run(ctx, cfg)
	require.NoError(t, err, "Run tests failed")

	// Verify output 1
	outPath := filepath.Join(realDawnRoot, "src", "tint", "test")
	content, err := osw.ReadFile(outPath)
	require.NoError(t, err, "Output file 1 not found")
	require.Contains(t, string(content), "Test Template 1", "Output content 1 mismatch")

	// Verify output 2
	outPath2 := filepath.Join(realDawnRoot, "test", "tint", "subdir", "test2")
	content2, err := osw.ReadFile(outPath2)
	require.NoError(t, err, "Output file 2 not found")
	require.Contains(t, string(content2), "Test Template 2", "Output content 2 mismatch")
}

func TestCmd_Run_StaleCheck(t *testing.T) {
	osw, cfg, realDawnRoot := setupRunFileTest(t)
	ctx := context.Background()

	tmplPath := filepath.Join(realDawnRoot, "src", "tint", "stale.tmpl")
	createTemplateFile(t, osw, tmplPath, `Stale Template`)

	// Create the output file with different content
	outPath := filepath.Join(realDawnRoot, "src", "tint", "stale")
	err := osw.WriteFile(outPath, []byte("Different Content"), 0644)
	require.NoError(t, err, "Failed to write existing output file")

	cfg.Flags.CheckStale = true
	c := &CmdSources{}
	err = c.Run(ctx, cfg)

	require.Error(t, err, "Run should have returned an error for stale files")
	staleFiles, ok := err.(common.StaleFiles)
	require.True(t, ok, "Error should be of type common.StaleFiles")
	require.Len(t, staleFiles, 1, "Should have 1 stale file")
	require.Equal(t, outPath, staleFiles[0], "Stale file path mismatch")
}

func TestCmd_Run_StaleCheck_Clean(t *testing.T) {
	osw, cfg, realDawnRoot := setupRunFileTest(t)
	ctx := context.Background()

	tmplPath := filepath.Join(realDawnRoot, "src", "tint", "clean.tmpl")
	createTemplateFile(t, osw, tmplPath, `Clean Template`)

	// First run: generate the file
	c := &CmdSources{}
	err := c.Run(ctx, cfg)
	require.NoError(t, err, "First run failed")

	// Verify the file exists
	outPath := filepath.Join(realDawnRoot, "src", "tint", "clean")
	_, err = osw.ReadFile(outPath)
	require.NoError(t, err, "Output file should exist after first run")

	// Second run: check for staleness
	cfg.Flags.CheckStale = true
	err = c.Run(ctx, cfg)
	require.NoError(t, err, "Second run (stale check) should not return error for clean file")
}

func TestCmd_Run_InvalidTemplateSyntax(t *testing.T) {
	osw, cfg, realDawnRoot := setupRunFileTest(t)
	ctx := context.Background()

	tmplPath := filepath.Join(realDawnRoot, "src", "tint", "invalid.tmpl")
	createTemplateFile(t, osw, tmplPath, `{{ invalid syntax }}`)

	c := &CmdSources{}
	err := c.Run(ctx, cfg)
	require.Error(t, err, "Run should fail with invalid template syntax")
	require.ErrorContains(t, err, "function \"invalid\" not defined")
}

func TestCmd_Run_MissingIntrinsicDef(t *testing.T) {
	osw, cfg, realDawnRoot := setupRunFileTest(t)
	ctx := context.Background()

	tmplPath := filepath.Join(realDawnRoot, "src", "tint", "missing_intrinsics.tmpl")
	// The template must try to use the intrinsics to trigger the load.
	createTemplateFile(t, osw, tmplPath, `{{ (LoadIntrinsics "src/tint/missing.def").Sem }}`)

	c := &CmdSources{}
	err := c.Run(ctx, cfg)
	require.Error(t, err, "Run should fail with missing intrinsic definition")
	// The error comes from ReadFile failing in intrinsicCache.Sem()
	require.ErrorContains(t, err, "does not exist")
}

// spyFS is a wrapper around FilesystemReader that records calls to ReadFile.
type spyFS struct {
	oswrapper.FilesystemReader
	readFileCounts map[string]int
	readFileErr    error
}

func newSpyFS(base oswrapper.FilesystemReader) *spyFS {
	return &spyFS{
		FilesystemReader: base,
		readFileCounts:   make(map[string]int),
	}
}

func (s *spyFS) ReadFile(name string) ([]byte, error) {
	s.readFileCounts[name]++
	if s.readFileErr != nil {
		return nil, s.readFileErr
	}
	return s.FilesystemReader.ReadFile(name)
}

func TestIntrinsicCache_Sem_Caching(t *testing.T) {
	osw, _, realDawnRoot := setupRunFileTest(t)
	spy := newSpyFS(osw)

	defPath := filepath.Join(realDawnRoot, "src/tint/intrinsics.def")
	defContent := `type T`
	createTemplateFile(t, osw, defPath, defContent)

	cache := &intrinsicCache{
		path:     "src/tint/intrinsics.def",
		fsReader: spy,
	}

	// First call should parse and cache
	sem1, err := cache.Sem()
	require.NoError(t, err)
	require.NotNil(t, sem1)
	require.Equal(t, 1, spy.readFileCounts[defPath])

	// Second call should return cached value without reading file
	sem2, err := cache.Sem()
	require.NoError(t, err)
	require.Equal(t, sem1, sem2)
	require.Equal(t, 1, spy.readFileCounts[defPath])
}

func TestIntrinsicCache_Sem_ReadFileError(t *testing.T) {
	osw, _, realDawnRoot := setupRunFileTest(t)
	spy := newSpyFS(osw)

	defPath := filepath.Join(realDawnRoot, "src/tint/intrinsics.def")
	// The file doesn't exist in osw, but also inject an error in spy
	// to ensure testing of the ReadFile error propagation specifically.
	expectedErr := filepath.ErrBadPattern // Just a distinctive error
	spy.readFileErr = expectedErr

	cache := &intrinsicCache{
		path:     "src/tint/intrinsics.def",
		fsReader: spy,
	}

	sem, err := cache.Sem()
	require.Error(t, err)
	require.Equal(t, expectedErr, err)
	require.Nil(t, sem)
	require.Equal(t, 1, spy.readFileCounts[defPath])
}

func TestIntrinsicCache_Sem_ParseError(t *testing.T) {
	osw, _, realDawnRoot := setupRunFileTest(t)

	defPath := filepath.Join(realDawnRoot, "src/tint/intrinsics.def")
	createTemplateFile(t, osw, defPath, `£`)

	cache := &intrinsicCache{
		path:     "src/tint/intrinsics.def",
		fsReader: osw,
	}

	sem, err := cache.Sem()
	require.Error(t, err)
	require.Nil(t, sem)
	require.ErrorContains(t, err, "src/tint/intrinsics.def:1:1")
}

func TestIntrinsicCache_Table_Caching(t *testing.T) {
	osw, _, realDawnRoot := setupRunFileTest(t)
	spy := newSpyFS(osw)

	defPath := filepath.Join(realDawnRoot, "src/tint/intrinsics.def")
	defContent := `type T`
	createTemplateFile(t, osw, defPath, defContent)

	cache := &intrinsicCache{
		path:     "src/tint/intrinsics.def",
		fsReader: spy,
	}

	// First call
	table1, err := cache.Table()
	require.NoError(t, err)
	require.NotNil(t, table1)
	require.NotNil(t, cache.cachedTable)
	require.Equal(t, 1, spy.readFileCounts[defPath])

	// Second call
	table2, err := cache.Table()
	require.NoError(t, err)
	require.Equal(t, table1, table2)
	require.Equal(t, 1, spy.readFileCounts[defPath])
}

func TestIntrinsicCache_Permute_Caching(t *testing.T) {
	osw, _, realDawnRoot := setupRunFileTest(t)
	spy := newSpyFS(osw)

	defPath := filepath.Join(realDawnRoot, "src/tint/intrinsics.def")
	// Define a simple function that can be permuted.
	defContent := `
fn F()
`
	createTemplateFile(t, osw, defPath, defContent)

	cache := &intrinsicCache{
		path:     "src/tint/intrinsics.def",
		fsReader: spy,
	}

	// Load semantics to get an overload to permute.
	s, err := cache.Sem()
	require.NoError(t, err)
	require.NotEmpty(t, s.Builtins)
	require.NotEmpty(t, s.Builtins[0].Overloads)
	require.Equal(t, 1, spy.readFileCounts[defPath])
	overload := s.Builtins[0].Overloads[0]

	// Reset cachedPermuter to ensure that the lazy initialization is tested.
	cache.cachedPermuter = nil

	perms1, err := cache.Permute(overload)
	require.NoError(t, err)
	require.NotNil(t, perms1)
	require.NotNil(t, cache.cachedPermuter)
	// Since cachedSem is already set, no new file read should happen.
	require.Equal(t, 1, spy.readFileCounts[defPath])

	perms2, err := cache.Permute(overload)
	require.NoError(t, err)
	require.Equal(t, perms1, perms2)
	// Still no file read.
	require.Equal(t, 1, spy.readFileCounts[defPath])
}

func TestGenCache_Intrinsics(t *testing.T) {
	osw, _, _ := setupRunFileTest(t)
	cache := &genCache{
		fsReader: osw,
	}

	// First call for path 1
	i1 := cache.intrinsics("path/to/def1")
	require.NotNil(t, i1)
	require.Equal(t, "path/to/def1", i1.path)
	require.Equal(t, osw, i1.fsReader)

	// Second call for path 1 should return same instance
	i2 := cache.intrinsics("path/to/def1")
	require.True(t, i1 == i2, "Should return the same instance for the same path")

	// Call for path 2 should return new instance
	i3 := cache.intrinsics("path/to/def2")
	require.NotNil(t, i3)
	require.Equal(t, "path/to/def2", i3.path)
	require.True(t, i1 != i3, "Should return different instance for different path")
}

func TestGenerator_Scramble(t *testing.T) {
	// Initialize generator with a fixed seed for deterministic behavior.
	g := generator{
		rnd: rand.New(rand.NewSource(123)),
	}

	initial := "InitialString"
	avoid := container.NewSet[string]()

	// Test 1: Verify scramble changes the string.
	scrambled, err := g.scramble(initial, avoid)
	require.NoError(t, err)
	require.NotEqual(t, initial, scrambled)

	// Test 2: Verify scramble avoids strings in |avoid|.
	avoid.Add(scrambled)
	scrambled2, err := g.scramble(initial, avoid)
	require.NoError(t, err)
	require.NotEqual(t, initial, scrambled2)
	require.NotEqual(t, scrambled, scrambled2)
	require.False(t, avoid.Contains(scrambled2))
}

func TestGenerator_SetCommentPrefix(t *testing.T) {
	g := generator{}
	require.Empty(t, g.commentPrefix)

	ret := g.setCommentPrefix("// New Prefix")
	require.Empty(t, ret, "setCommentPrefix should return empty string")
	require.Equal(t, "// New Prefix", g.commentPrefix)
}

func TestHelper_Is(t *testing.T) {
	// Test with sem.EnumEntry
	isEnumEntry := is(sem.EnumEntry{})
	require.True(t, isEnumEntry(sem.EnumEntry{}), "Should be true for sem.EnumEntry")
	require.True(t, isEnumEntry(&sem.EnumEntry{}), "Should be true for *sem.EnumEntry")
	require.False(t, isEnumEntry(sem.EnumMatcher{}), "Should be false for sem.EnumMatcher")

	// Test with basic types
	isInt := is(1)
	require.True(t, isInt(1), "Should be true for int")
	require.True(t, isInt(100), "Should be true for int")
	val := 5
	require.True(t, isInt(&val), "Should be true for *int")
	require.False(t, isInt("string"), "Should be false for string")
}

func TestHelper_IsFirstIn(t *testing.T) {
	slice := []int{1, 2, 3}
	require.True(t, isFirstIn(1, slice), "1 should be first in [1, 2, 3]")
	require.False(t, isFirstIn(2, slice), "2 should not be first in [1, 2, 3]")

	empty := []int{}
	require.False(t, isFirstIn(1, empty), "Should be false for empty slice")
}

func TestHelper_IsLastIn(t *testing.T) {
	slice := []int{1, 2, 3}
	require.True(t, isLastIn(3, slice), "3 should be last in [1, 2, 3]")
	require.False(t, isLastIn(2, slice), "2 should not be last in [1, 2, 3]")

	empty := []int{}
	require.False(t, isLastIn(1, empty), "Should be false for empty slice")
}
