package mcp

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

	"github.com/stretchr/testify/require"

	"github.com/zzet/gortex/internal/graph"
)

// --- env_var_users ---------------------------------------------------------

func TestAnalyzeEnvVarUsers_FiltersToEnvKeys(t *testing.T) {
	srv, _ := setupTestServer(t)
	addConfigKeyNode(srv.graph, "cfg::env::DATABASE_URL", "DATABASE_URL", "env")
	addConfigKeyNode(srv.graph, "viper::log.level", "log.level", "viper")
	addReadConfigEdge(srv.graph, "f.go::A", "cfg::env::DATABASE_URL")
	addReadConfigEdge(srv.graph, "f.go::B", "cfg::env::DATABASE_URL")
	addReadConfigEdge(srv.graph, "f.go::C", "viper::log.level")

	out := callAnalyze(t, srv, "env_var_users", map[string]any{})
	rows, _ := out["env_vars"].([]any)
	require.Len(t, rows, 1, "viper (non-env) config key must be excluded")
	row := rows[0].(map[string]any)
	require.Equal(t, "DATABASE_URL", row["name"])
	require.Equal(t, float64(2), row["reads"])
}

func TestAnalyzeEnvVarUsers_NameFilter(t *testing.T) {
	srv, _ := setupTestServer(t)
	addConfigKeyNode(srv.graph, "env::PORT", "PORT", "env")
	addConfigKeyNode(srv.graph, "env::HOST", "HOST", "env")
	addReadConfigEdge(srv.graph, "f.go::A", "env::PORT")
	addReadConfigEdge(srv.graph, "f.go::B", "env::HOST")

	out := callAnalyze(t, srv, "env_var_users", map[string]any{"name": "port"})
	require.Equal(t, float64(1), out["total"])
}

// --- sql_call_sites --------------------------------------------------------

func TestAnalyzeSQLCallSites_GroupsByCaller(t *testing.T) {
	srv, _ := setupTestServer(t)
	srv.graph.AddNode(&graph.Node{ID: "f.go::GetUser", Kind: graph.KindFunction, Name: "GetUser", FilePath: "f.go"})
	srv.graph.AddNode(&graph.Node{ID: "tbl::users", Kind: graph.KindTable, Name: "users"})
	srv.graph.AddNode(&graph.Node{ID: "tbl::orders", Kind: graph.KindTable, Name: "orders"})
	srv.graph.AddEdge(&graph.Edge{From: "f.go::GetUser", To: "tbl::users", Kind: graph.EdgeQueries, Meta: map[string]any{"op": "read"}})
	srv.graph.AddEdge(&graph.Edge{From: "f.go::GetUser", To: "tbl::orders", Kind: graph.EdgeQueries, Meta: map[string]any{"op": "write"}})

	out := callAnalyze(t, srv, "sql_call_sites", map[string]any{})
	rows, _ := out["call_sites"].([]any)
	require.Len(t, rows, 1)
	row := rows[0].(map[string]any)
	require.Equal(t, "GetUser", row["name"])
	require.Equal(t, float64(2), row["queries"])
	require.Equal(t, float64(1), row["reads"])
	require.Equal(t, float64(1), row["writes"])
}

// --- fixes_history ---------------------------------------------------------

func mustGit(t *testing.T, dir string, args ...string) {
	t.Helper()
	cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
	cmd.Env = append(cmd.Environ(),
		"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t.test",
		"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t.test")
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("git %v: %v\n%s", args, err, out)
	}
}

func TestFixSubjectRe(t *testing.T) {
	for _, s := range []string{"fix: x", "fixes #3", "fixed the bug", "bugfix in y", "hotfix", "FIX something"} {
		require.True(t, fixSubjectRe.MatchString(s), "%q should be a fix subject", s)
	}
	for _, s := range []string{"add feature", "prefix handling", "refactor fixtures", "update docs"} {
		require.False(t, fixSubjectRe.MatchString(s), "%q should NOT be a fix subject", s)
	}
}

func TestMineFixCommits_DetectsFixSubjects(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}
	dir := t.TempDir()
	mustGit(t, dir,"init", "-q")
	commit := func(body, msg string) {
		require.NoError(t, os.WriteFile(filepath.Join(dir, "a.go"), []byte(body), 0o644))
		mustGit(t, dir,"add", "a.go")
		mustGit(t, dir,"commit", "-q", "-m", msg)
	}
	commit("package a\n", "add feature a")
	commit("package a\n// v2\n", "fix: nil deref in a")
	commit("package a\n// v3\n", "fixes crash on startup")

	commits := mineFixCommits(context.Background(), dir, 100)
	require.Len(t, commits, 2, "two fix commits, the plain-feature commit excluded")
	for _, c := range commits {
		require.Contains(t, c.files, "a.go")
	}
}

func TestAnalyzeFixesHistory_EndToEnd(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}
	srv, dir := setupTestServer(t)
	mustGit(t, dir,"init", "-q")
	mustGit(t, dir,"add", "main.go")
	mustGit(t, dir,"commit", "-q", "-m", "initial commit")
	require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"),
		[]byte("package main\n\nfunc main() {}\n"), 0o644))
	mustGit(t, dir,"add", "main.go")
	mustGit(t, dir,"commit", "-q", "-m", "fix: correct main logic")

	out := callAnalyze(t, srv, "fixes_history", map[string]any{})
	if got, _ := out["total_fix_commits"].(float64); got < 1 {
		t.Fatalf("expected >=1 fix commit, got %v", got)
	}
	files, _ := out["files"].([]any)
	require.NotEmpty(t, files)
	require.Equal(t, "main.go", files[0].(map[string]any)["file"])
}

func TestSymbolNamesInFile(t *testing.T) {
	srv, _ := setupTestServer(t)
	// setupTestServer indexes a main.go containing main + helper.
	require.NotEmpty(t, srv.symbolNamesInFile("main.go"))
}

// --- edge_audit ------------------------------------------------------------

func TestAnalyzeEdgeAudit_Buckets(t *testing.T) {
	srv, _ := setupTestServer(t)
	g := srv.graph

	// An interface with no implementor.
	g.AddNode(&graph.Node{ID: "z.go::Orphan", Kind: graph.KindInterface, Name: "Orphan"})
	// An interface with an implementor — must NOT be flagged.
	g.AddNode(&graph.Node{ID: "z.go::Used", Kind: graph.KindInterface, Name: "Used"})
	g.AddNode(&graph.Node{ID: "z.go::Impl", Kind: graph.KindType, Name: "Impl"})
	g.AddEdge(&graph.Edge{From: "z.go::Impl", To: "z.go::Used", Kind: graph.EdgeImplements})
	// A function reached only from a test file, via a text-matched edge.
	g.AddNode(&graph.Node{ID: "z.go::Helper", Kind: graph.KindFunction, Name: "Helper", FilePath: "z.go"})
	g.AddNode(&graph.Node{ID: "z_test.go::TestZ", Kind: graph.KindFunction, Name: "TestZ", FilePath: "z_test.go"})
	g.AddEdge(&graph.Edge{
		From: "z_test.go::TestZ", To: "z.go::Helper",
		Kind: graph.EdgeCalls, Origin: graph.OriginTextMatched,
	})

	out := callAnalyze(t, srv, "edge_audit", map[string]any{})
	require.Contains(t, out, "edge_tiers")
	require.Contains(t, out, "summary")

	ui := out["unimplemented_interfaces"].(map[string]any)
	require.GreaterOrEqual(t, ui["count"].(float64), float64(1))
	require.NotContains(t, sampleStrings(ui), "z.go::Used")

	to := out["test_only_targets"].(map[string]any)
	require.Contains(t, sampleStrings(to), "z.go::Helper")

	wc := out["weak_call_edges"].(map[string]any)
	require.GreaterOrEqual(t, wc["count"].(float64), float64(1))
}

func sampleStrings(bucket map[string]any) []string {
	raw, _ := bucket["sample"].([]any)
	out := make([]string, 0, len(raw))
	for _, v := range raw {
		if s, ok := v.(string); ok {
			out = append(out, s)
		}
	}
	return out
}
