package mcp

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zzet/gortex/internal/graph"
)

func newGenerateSkillTestServer(t *testing.T) *Server {
	t.Helper()
	return &Server{
		graph:      graph.New(),
		session:    newSessionState(),
		tokenStats: &tokenStats{},
		symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
		sessions:   newSessionMap(),
		toolScopes: newScopeRegistry(),
	}
}

// seedSkillSource builds a small source tree the tool will bundle.
// Returns the absolute path to the directory.
func seedSkillSource(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"),
		[]byte("package main\n\nfunc Run() int {\n\tfor i := 0; i < 5; i++ {\n\t\t_ = i\n\t}\n\treturn 42\n}\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "helpers.go"),
		[]byte("package main\n\nfunc Helper() string { return \"hi\" }\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"),
		[]byte("# Demo project\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(dir, "node_modules"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "node_modules", "junk.js"),
		[]byte("// noise\n"), 0o644))
	return dir
}

func callGenerateSkill(t *testing.T, s *Server, args map[string]any) map[string]any {
	t.Helper()
	req := mcp.CallToolRequest{}
	req.Params.Arguments = args
	res, err := s.handleGenerateSkill(context.Background(), req)
	require.NoError(t, err)
	require.NotNil(t, res)
	if res.IsError {
		return map[string]any{"is_error": true, "content": res.Content}
	}
	tc, ok := res.Content[0].(mcp.TextContent)
	require.True(t, ok)
	var m map[string]any
	require.NoError(t, json.Unmarshal([]byte(tc.Text), &m))
	return m
}

func TestGenerateSkill_HappyPath(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	srcDir := seedSkillSource(t)
	outDir := t.TempDir()

	out := callGenerateSkill(t, s, map[string]any{
		"directory":  srcDir,
		"output_dir": outDir,
	})

	skillPath := out["skill_path"].(string)
	require.FileExists(t, skillPath)
	body, _ := os.ReadFile(skillPath)
	assert.Contains(t, string(body), "name:", "frontmatter present")
	assert.Contains(t, string(body), "description:", "frontmatter present")
	assert.Contains(t, string(body), "## References", "reference section present")

	// References tree exists with the source files but not node_modules.
	assert.FileExists(t, filepath.Join(outDir, "references", "main.go"))
	assert.FileExists(t, filepath.Join(outDir, "references", "helpers.go"))
	assert.NoFileExists(t, filepath.Join(outDir, "references", "node_modules", "junk.js"))
}

func TestGenerateSkill_SluggifiesDefaultName(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	// Directory with spaces and special chars.
	parent := t.TempDir()
	srcDir := filepath.Join(parent, "My Demo Project!")
	require.NoError(t, os.MkdirAll(srcDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(srcDir, "x.go"), []byte("package x\n"), 0o644))

	out := callGenerateSkill(t, s, map[string]any{
		"directory":  srcDir,
		"output_dir": filepath.Join(t.TempDir(), "out"),
	})
	assert.Equal(t, "my-demo-project", out["skill_name"])
}

func TestGenerateSkill_IncludeFilter(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	srcDir := seedSkillSource(t)
	outDir := t.TempDir()

	out := callGenerateSkill(t, s, map[string]any{
		"directory":        srcDir,
		"output_dir":       outDir,
		"include_patterns": "*.go",
	})

	refs, _ := out["references"].([]any)
	assert.NotEmpty(t, refs)
	for _, r := range refs {
		m := r.(map[string]any)
		assert.True(t, strings.HasSuffix(m["rel_path"].(string), ".go"))
	}
}

func TestGenerateSkill_IgnoreFilter(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	srcDir := seedSkillSource(t)
	outDir := t.TempDir()

	out := callGenerateSkill(t, s, map[string]any{
		"directory":       srcDir,
		"output_dir":      outDir,
		"ignore_patterns": "helpers.go",
	})

	refs, _ := out["references"].([]any)
	for _, r := range refs {
		m := r.(map[string]any)
		assert.NotEqual(t, "helpers.go", m["rel_path"])
	}
}

func TestGenerateSkill_DryRunWritesNothing(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	srcDir := seedSkillSource(t)
	outDir := filepath.Join(t.TempDir(), "out")

	out := callGenerateSkill(t, s, map[string]any{
		"directory":  srcDir,
		"output_dir": outDir,
		"dry_run":    true,
	})

	assert.Equal(t, true, out["dry_run"])
	_, statErr := os.Stat(outDir)
	assert.True(t, os.IsNotExist(statErr), "dry_run must not create output dir")

	refs, _ := out["references"].([]any)
	assert.NotEmpty(t, refs, "dry_run still reports the candidate references")
}

func TestGenerateSkill_CompressBraceBodies(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	srcDir := seedSkillSource(t)
	outDir := t.TempDir()

	out := callGenerateSkill(t, s, map[string]any{
		"directory":  srcDir,
		"output_dir": outDir,
		"compress":   true,
	})

	body, _ := os.ReadFile(filepath.Join(outDir, "references", "main.go"))
	assert.Contains(t, string(body), "lines elided", "Run's body should be elided")
	assert.Contains(t, string(body), "func Run()", "signature kept")

	// At least one reference should report compressed=true.
	refs, _ := out["references"].([]any)
	hasCompressed := false
	for _, r := range refs {
		if r.(map[string]any)["compressed"] == true {
			hasCompressed = true
			break
		}
	}
	assert.True(t, hasCompressed)
}

func TestGenerateSkill_MaxReferencesCap(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	srcDir := t.TempDir()
	for i := range 10 {
		require.NoError(t, os.WriteFile(filepath.Join(srcDir, "f"+string(rune('A'+i))+".go"),
			[]byte("package x\n"), 0o644))
	}

	out := callGenerateSkill(t, s, map[string]any{
		"directory":      srcDir,
		"output_dir":     t.TempDir(),
		"max_references": 3,
	})
	refs, _ := out["references"].([]any)
	assert.LessOrEqual(t, len(refs), 3)
	assert.GreaterOrEqual(t, out["skipped"].(float64), 7.0)
}

func TestGenerateSkill_DefaultsToClaudeSkillsPath(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	// Init a git repo so repoRootContaining resolves the root.
	dir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
	srcDir := filepath.Join(dir, "src")
	require.NoError(t, os.MkdirAll(srcDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(srcDir, "a.go"), []byte("package x\n"), 0o644))

	out := callGenerateSkill(t, s, map[string]any{
		"directory":  srcDir,
		"skill_name": "demo",
	})

	expectedSkillDir := filepath.Join(dir, ".claude", "skills", "demo")
	assert.Equal(t, expectedSkillDir, out["output_dir"])
	assert.FileExists(t, filepath.Join(expectedSkillDir, "SKILL.md"))
}

func TestGenerateSkill_RejectsMissingDirectory(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	out := callGenerateSkill(t, s, map[string]any{})
	assert.True(t, out["is_error"] == true)
}

func TestGenerateSkill_RejectsNonDirectory(t *testing.T) {
	s := newGenerateSkillTestServer(t)
	dir := t.TempDir()
	file := filepath.Join(dir, "not_a_dir.txt")
	require.NoError(t, os.WriteFile(file, []byte("x"), 0o644))
	out := callGenerateSkill(t, s, map[string]any{"directory": file})
	assert.True(t, out["is_error"] == true)
}

// Unit-level checks for the helpers.

func TestSluggify(t *testing.T) {
	cases := map[string]string{
		"my-skill":           "my-skill",
		"My Skill":           "my-skill",
		"my!skill!":          "my-skill",
		"  spaces  ":         "spaces",
		"My_skill.123":       "my-skill-123",
	}
	for in, want := range cases {
		assert.Equal(t, want, sluggify(in), "sluggify(%q)", in)
	}
}

func TestElideBraceBodies_KeepsSignatures(t *testing.T) {
	src := "func A() {\n\tfor i := 0; i < 10; i++ {\n\t\tdoWork(i)\n\t}\n}\n\nfunc B() int { return 1 }\n"
	out := elideBraceBodies(src)
	assert.Contains(t, out, "func A()")
	assert.Contains(t, out, "lines elided")
	// One-line body should stay verbatim.
	assert.Contains(t, out, "{ return 1 }")
}

func TestMatchPathPattern(t *testing.T) {
	assert.True(t, matchPathPattern("*.go", "foo.go"))
	assert.True(t, matchPathPattern("*.go", "sub/foo.go"))
	assert.True(t, matchPathPattern("vendor", "vendor/x.go"))
	assert.True(t, matchPathPattern("vendor/*", "vendor/x.go"))
	assert.False(t, matchPathPattern("*.ts", "foo.go"))
}
