package scanner

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestScanMergesIgnoreRulesAndIsDeterministic(t *testing.T) {
	root := t.TempDir()
	writeFile(t, root, ".gitignore", "ignored.log\nvendor/\n")
	writeFile(t, root, ".agentteam-insightignore", "secret/**\nimportant.txt\n!important.txt\n")
	writeFile(t, root, "main.go", "package main\n\nfunc main() {}\n")
	writeFile(t, root, "README.md", "# Project\n")
	writeFile(t, root, "ignored.log", "ignored\n")
	writeFile(t, root, "vendor/mod/file.go", "package vendored\n")
	writeFile(t, root, "secret/a.go", "package secret\n")
	writeFile(t, root, "important.txt", "keep\n")
	writeFile(t, root, ".agentteam-insight/.trash-old/old.json", "{}\n")
	writeFile(t, root, ".ua/intermediate/batch-1.json", "{}\n")
	writeFile(t, root, ".ua/tmp/scan.json", "{}\n")
	writeFile(t, root, ".ua/.trash-old/batch.json", "{}\n")
	writeFile(t, root, ".ua/knowledge-graph.json", "{}\n")
	writeFile(t, root, "bin.dat", string([]byte{0, 1, 2, 3}))

	first, err := Scan(context.Background(), Options{Root: root})
	require.NoError(t, err)
	second, err := Scan(context.Background(), Options{Root: root})
	require.NoError(t, err)
	require.Equal(t, first.Files, second.Files)
	require.Equal(t, []string{"go", "markdown"}, first.Languages)
	require.Equal(t, 1, first.FilteredByIgnore)
	requireFilePaths(t, first.Files, []string{"README.md", "important.txt", "main.go"})
	requireIgnored(t, first.Ignored, "ignored.log", "ignore pattern ignored.log from .gitignore")
	requireIgnored(t, first.Ignored, "secret", "ignore pattern secret/** from .agentteam-insightignore")
	requireIgnored(t, first.Ignored, "vendor", "dependency directory")
	requireIgnored(t, first.Ignored, ".agentteam-insight", "AgentTeam Insight analyzer-owned directory")
	requireIgnored(t, first.Ignored, ".ua", "Understand Anything analyzer-owned directory")
	requireIgnored(t, first.Ignored, "bin.dat", "binary file")

	mainFile := findFile(t, first.Files, "main.go")
	require.Equal(t, int64(len("package main\n\nfunc main() {}\n")), mainFile.Size)
	require.Equal(t, 3, mainFile.Lines)
	require.Equal(t, "go", mainFile.Language)
	require.Equal(t, CategoryCode, mainFile.Category)
	require.Len(t, mainFile.ContentHash, 64)
	require.Equal(t, len(first.Files), first.TotalFiles)
	require.Equal(t, "small", first.EstimatedComplexity)
	requireIgnored(t, first.Ignored, ".agentteam-insightignore", "graph builder ignore file")
}

func TestApplyNarrativeSkipsReadmeBadges(t *testing.T) {
	root := t.TempDir()
	writeFile(t, root, "go.mod", "module github.com/pingcap/failpoint\n")
	writeFile(t, root, "README.md", "# failpoint\n\n[![LICENSE](https://img.shields.io/badge/license-Apache-blue.svg)](LICENSE)\n\nAn implementation of failpoints for Go.\n")
	result := Result{}

	ApplyNarrative(root, &result)

	require.Equal(t, "An implementation of failpoints for Go.", result.Description)
}

func TestApplyNarrativeKeepsRuneSafeFirst3000READMECharacters(t *testing.T) {
	root := t.TempDir()
	writeFile(t, root, "README.md", strings.Repeat("界", 3001)+"tail")
	var result Result

	ApplyNarrative(root, &result)

	require.Equal(t, 3000, len([]rune(result.ReadmeContent)))
	require.NotContains(t, result.ReadmeContent, "tail")
}

func TestClassifyPath(t *testing.T) {
	language, category := ClassifyPath(".github/workflows/ci.yml")
	require.Equal(t, "github-actions", language)
	require.Equal(t, CategoryInfra, category)
	language, category = ClassifyPath("deploy/main.tf")
	require.Equal(t, "terraform", language)
	require.Equal(t, CategoryInfra, category)
	language, category = ClassifyPath("docs/guide.md")
	require.Equal(t, "markdown", language)
	require.Equal(t, CategoryDocs, category)
	language, category = ClassifyPath("src/App.kt")
	require.Equal(t, "kotlin", language)
	require.Equal(t, CategoryCode, category)
	language, category = ClassifyPath("src/App.cs")
	require.Equal(t, "csharp", language)
	require.Equal(t, CategoryCode, category)
	language, category = ClassifyPath("src/app.rb")
	require.Equal(t, "ruby", language)
	require.Equal(t, CategoryCode, category)
	language, category = ClassifyPath("src/unknown.xyz")
	require.Equal(t, "xyz", language)
	require.Equal(t, CategoryCode, category)
	language, category = ClassifyPath("NOEXT")
	require.Equal(t, "unknown", language)
	require.Equal(t, CategoryCode, category)
}

func writeFile(t *testing.T, root string, rel string, content string) {
	t.Helper()
	path := filepath.Join(root, filepath.FromSlash(rel))
	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
	require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}

func requireFilePaths(t *testing.T, files []FileInfo, expected []string) {
	t.Helper()
	actual := make([]string, 0, len(files))
	for _, file := range files {
		actual = append(actual, file.Path)
	}
	require.Equal(t, expected, actual)
}

func requireIgnored(t *testing.T, ignored []IgnoreInfo, path string, reason string) {
	t.Helper()
	for _, item := range ignored {
		if item.Path == path {
			require.Equal(t, reason, item.Reason)
			return
		}
	}
	t.Fatalf("missing ignored path %q in %#v", path, ignored)
}

func findFile(t *testing.T, files []FileInfo, path string) FileInfo {
	t.Helper()
	for _, file := range files {
		if file.Path == path {
			return file
		}
	}
	t.Fatalf("missing file %q", path)
	return FileInfo{}
}

func TestScanUsesGitLsFilesAndStandardGraphIgnore(t *testing.T) {
	root := t.TempDir()
	runGit(t, root, "init")
	runGit(t, root, "config", "user.email", "test@example.com")
	runGit(t, root, "config", "user.name", "Test")
	writeFile(t, root, ".gitignore", "dist/**\n!dist/keep.go\nlogs/**/*.log\n")
	writeFile(t, root, ".agentteam-insightignore", "secret/**\n")
	writeFile(t, root, "main.go", "package main\n")
	writeFile(t, root, "dist/drop.go", "package dist\n")
	writeFile(t, root, "dist/keep.go", "package dist\n")
	writeFile(t, root, "logs/nested/debug.log", "debug\n")
	writeFile(t, root, "secret/drop.go", "package secret\n")
	writeFile(t, root, "untracked.go", "package main\n")
	runGit(t, root, "add", ".gitignore", "main.go", "dist/keep.go")
	runGit(t, root, "commit", "-m", "init")

	result, err := Scan(context.Background(), Options{Root: root})
	require.NoError(t, err)
	requireFilePaths(t, result.Files, []string{"dist/keep.go", "main.go", "untracked.go"})
	requireIgnored(t, result.Ignored, "secret/drop.go", "ignore pattern secret/** from .agentteam-insightignore")
	require.Equal(t, 1, result.FilteredByIgnore)
}

func TestScanFilesystemExcludesAnalyzerOwnedDirectories(t *testing.T) {
	root := t.TempDir()
	writeAnalyzerDirectoryFixture(t, root)
	result, err := Scan(context.Background(), Options{Root: root})
	require.NoError(t, err)
	requireFilePaths(t, result.Files, []string{"main.go"})
	requireIgnored(t, result.Ignored, ".ua", "Understand Anything analyzer-owned directory")
	requireIgnored(t, result.Ignored, ".agentteam-insight", "AgentTeam Insight analyzer-owned directory")
}

func TestScanGitExcludesAnalyzerOwnedDirectories(t *testing.T) {
	root := t.TempDir()
	runGit(t, root, "init")
	runGit(t, root, "config", "user.email", "test@example.com")
	runGit(t, root, "config", "user.name", "Test")
	writeAnalyzerDirectoryFixture(t, root)
	runGit(t, root, "add", "-f", ".")
	result, err := Scan(context.Background(), Options{Root: root})
	require.NoError(t, err)
	requireFilePaths(t, result.Files, []string{"main.go"})
}

func writeAnalyzerDirectoryFixture(t *testing.T, root string) {
	t.Helper()
	writeFile(t, root, "main.go", "package main\n")
	writeFile(t, root, ".ua/config.json", "{}\n")
	writeFile(t, root, ".ua/.understandignore", "generated\n")
	writeFile(t, root, ".ua/intermediate/batch-1.json", "{}\n")
	writeFile(t, root, ".agentteam-insight/config.json", "{}\n")
	writeFile(t, root, ".agentteam-insight/knowledge-graph.json", "{}\n")
}

func TestGenerateStarterIgnoreFile(t *testing.T) {
	root := t.TempDir()
	writeFile(t, root, ".gitignore", "custom.out\nnode_modules/\n")
	require.NoError(t, os.MkdirAll(filepath.Join(root, "tests"), 0o755))
	created, path, err := EnsureStarterIgnore(root)
	require.NoError(t, err)
	require.True(t, created)
	require.Equal(t, filepath.Join(root, ".agentteam-insightignore"), path)
	data, err := os.ReadFile(path)
	require.NoError(t, err)
	text := string(data)
	require.Contains(t, text, "# .agentteam-insightignore")
	require.Contains(t, text, "# custom.out")
	require.NotContains(t, text, "# node_modules/")
	require.Contains(t, text, "# tests/")
	require.Contains(t, text, "# **/*_test.go")

	created, _, err = EnsureStarterIgnore(root)
	require.NoError(t, err)
	require.False(t, created)
}

func runGit(t *testing.T, root string, args ...string) {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = root
	out, err := cmd.CombinedOutput()
	require.NoError(t, err, string(out))
}
