package wiki

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

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

// buildToyGraph stands up a tiny graph with two communities so the
// generator has something to render. Five nodes, three calls, two
// files — enough to populate every page section.
func buildToyGraph() *graph.Graph {
	g := graph.New()
	for _, n := range []*graph.Node{
		{ID: "pkg/api.go::Handler", Kind: graph.KindFunction, Name: "Handler", FilePath: "pkg/api.go", StartLine: 10, Language: "go"},
		{ID: "pkg/api.go::dispatch", Kind: graph.KindFunction, Name: "dispatch", FilePath: "pkg/api.go", StartLine: 20, Language: "go"},
		{ID: "pkg/api.go::respond", Kind: graph.KindFunction, Name: "respond", FilePath: "pkg/api.go", StartLine: 30, Language: "go"},
		{ID: "pkg/store.go::Save", Kind: graph.KindFunction, Name: "Save", FilePath: "pkg/store.go", StartLine: 10, Language: "go"},
		{ID: "pkg/store.go::Load", Kind: graph.KindFunction, Name: "Load", FilePath: "pkg/store.go", StartLine: 20, Language: "go"},
	} {
		g.AddNode(n)
	}
	for _, e := range []*graph.Edge{
		{From: "pkg/api.go::Handler", To: "pkg/api.go::dispatch", Kind: graph.EdgeCalls, Origin: "ast_resolved"},
		{From: "pkg/api.go::dispatch", To: "pkg/api.go::respond", Kind: graph.EdgeCalls, Origin: "ast_resolved"},
		{From: "pkg/api.go::dispatch", To: "pkg/store.go::Save", Kind: graph.EdgeCalls, Origin: "lsp_resolved"},
		{From: "pkg/api.go::dispatch", To: "pkg/store.go::Load", Kind: graph.EdgeCalls, Origin: "lsp_resolved"},
	} {
		g.AddEdge(e)
	}
	return g
}

func TestGenerator_BasicFlow(t *testing.T) {
	g := buildToyGraph()
	communities := analysis.DetectCommunities(g)
	procs := analysis.DiscoverProcesses(g)
	hotspots := analysis.FindHotspots(g, communities, 0)
	cycles := analysis.DetectCycles(g, communities, "")

	tmp := t.TempDir()
	gen := New(Inputs{
		Graph:       g,
		Communities: communities,
		Processes:   procs,
		Hotspots:    hotspots,
		Cycles:      cycles,
	}, Options{
		OutputDir:    tmp,
		Repo:         "toy",
		MinCommunity: 1,
	})

	result, _, err := gen.Generate(context.Background())
	if err != nil {
		t.Fatalf("Generate: %v", err)
	}
	if result.OutputDir != tmp {
		t.Errorf("OutputDir = %q, want %q", result.OutputDir, tmp)
	}
	if len(result.Files) == 0 {
		t.Fatal("Generate returned no files")
	}
	if !strings.Contains(result.IndexMarkdown, "toy") {
		t.Errorf("top-level index should mention repo slug; got %q", result.IndexMarkdown)
	}
	if !strings.Contains(result.RepoIndexMarkdown, "Auto-generated by") {
		t.Errorf("repo index should contain auto-generated header; got %q", result.RepoIndexMarkdown)
	}

	// Spot-check the on-disk files. The architecture page must
	// embed a Mermaid block; the contracts page exists even with
	// zero contracts.
	mustExist := []string{
		filepath.Join(tmp, "index.md"),
		filepath.Join(tmp, "toy", "index.md"),
		filepath.Join(tmp, "toy", "architecture.md"),
		filepath.Join(tmp, "toy", "analysis", "hotspots.md"),
		filepath.Join(tmp, "toy", "analysis", "cycles.md"),
		filepath.Join(tmp, "toy", "analysis", "semantic.md"),
		filepath.Join(tmp, "toy", "contracts", "api-surface.md"),
		filepath.Join(tmp, "toy", "_assets", "community-graph.mermaid"),
		filepath.Join(tmp, "_workspace", "README.md"),
	}
	files := gen.lastWriterFiles()
	for _, want := range mustExist {
		rel, _ := filepath.Rel(tmp, want)
		if _, ok := files[filepath.ToSlash(rel)]; !ok {
			t.Errorf("expected wiki to materialise %q", want)
		}
	}
}

func TestGenerator_Idempotent(t *testing.T) {
	g := buildToyGraph()
	communities := analysis.DetectCommunities(g)
	procs := analysis.DiscoverProcesses(g)

	tmp := t.TempDir()
	mk := func() *Result {
		gen := New(Inputs{
			Graph:       g,
			Communities: communities,
			Processes:   procs,
		}, Options{OutputDir: tmp, Repo: "toy", MinCommunity: 1})
		r, _, err := gen.Generate(context.Background())
		if err != nil {
			t.Fatalf("Generate: %v", err)
		}
		return r
	}
	first := mk()
	second := mk()
	if len(first.Files) != len(second.Files) {
		t.Fatalf("file count differs: %d → %d", len(first.Files), len(second.Files))
	}
	for i := range first.Files {
		if first.Files[i].Path != second.Files[i].Path {
			t.Errorf("file %d path differs: %q → %q", i, first.Files[i].Path, second.Files[i].Path)
		}
		if first.Files[i].SHA256 != second.Files[i].SHA256 {
			t.Errorf("file %d hash differs (%s): %s → %s",
				i, first.Files[i].Path, first.Files[i].SHA256, second.Files[i].SHA256)
		}
	}
}

func TestGenerator_SequenceDiagramEmitted(t *testing.T) {
	g := buildToyGraph()
	communities := analysis.DetectCommunities(g)
	procs := analysis.DiscoverProcesses(g)
	if len(procs.Processes) == 0 {
		t.Fatal("toy graph should produce at least one process")
	}
	tmp := t.TempDir()
	gen := New(Inputs{Graph: g, Communities: communities, Processes: procs},
		Options{OutputDir: tmp, Repo: "toy", MinCommunity: 1})
	if _, _, err := gen.Generate(context.Background()); err != nil {
		t.Fatalf("Generate: %v", err)
	}
	files := gen.lastWriterFiles()
	hasSequence := false
	for path, body := range files {
		if strings.HasPrefix(path, "toy/processes/") && strings.HasSuffix(path, ".md") {
			if strings.Contains(string(body), "sequenceDiagram") &&
				(strings.Contains(string(body), "->>") || strings.Contains(string(body), "Note over")) {
				hasSequence = true
			}
		}
	}
	if !hasSequence {
		t.Error("expected at least one process page with a sequenceDiagram block")
	}
}

func TestGenerator_NoopEnhancerPreservesBody(t *testing.T) {
	g := buildToyGraph()
	communities := analysis.DetectCommunities(g)
	tmp := t.TempDir()
	gen := New(Inputs{Graph: g, Communities: communities},
		Options{OutputDir: tmp, Repo: "toy", MinCommunity: 1, Enhance: true, Enhancer: NoopEnhancer{}})
	r, _, err := gen.Generate(context.Background())
	if err != nil {
		t.Fatalf("Generate: %v", err)
	}
	if !strings.Contains(r.RepoIndexMarkdown, "Auto-generated by") {
		t.Error("NoopEnhancer should preserve template content")
	}
}

func TestSlugify(t *testing.T) {
	for in, want := range map[string]string{
		"FooBar":        "foobar",
		"foo bar baz":   "foo-bar-baz",
		"  weird/_-id ": "weird-id",
		"":              "",
	} {
		if got := slugify(in); got != want {
			t.Errorf("slugify(%q) = %q, want %q", in, got, want)
		}
	}
}

func TestRepoSlugFromPath(t *testing.T) {
	if got := RepoSlugFromPath("/abs/path/to/Repo"); got != "repo" {
		t.Errorf("RepoSlugFromPath = %q, want %q", got, "repo")
	}
	if got := RepoSlugFromPath(""); got != "repo" {
		t.Errorf("RepoSlugFromPath(\"\") = %q, want %q", got, "repo")
	}
}

// lastWriterFiles is a test-only accessor that re-runs the generator to
// capture the in-memory file payload. Cheaper than reading the disk
// and orderless.
func (gen *Generator) lastWriterFiles() map[string][]byte {
	w := NewWriter(gen.opts.OutputDir)
	ctx := context.Background()
	repoSlug := gen.opts.Repo
	if repoSlug == "" {
		repoSlug = "repo"
	}
	// Re-render every page into the test writer.
	repoIndex, _ := gen.renderRepoIndex(ctx)
	w.Write(filepath.ToSlash(filepath.Join(repoSlug, "index.md")), []byte(repoIndex))
	w.Write("index.md", []byte(gen.renderTopLevelIndex()))
	arch, _ := gen.renderArchitecturePage(ctx)
	w.Write(filepath.ToSlash(filepath.Join(repoSlug, "architecture.md")), []byte(arch))
	for _, c := range gen.kept {
		body, _ := gen.renderCommunityPage(ctx, c)
		w.Write(filepath.ToSlash(filepath.Join(repoSlug, gen.communityPagePath(c))), []byte(body))
	}
	if !gen.opts.NoProcesses && gen.processes != nil {
		for _, p := range gen.processes.Processes {
			body, _ := gen.renderProcessPage(ctx, p)
			w.Write(filepath.ToSlash(filepath.Join(repoSlug, processPagePath(p))), []byte(body))
		}
	}
	if !gen.opts.NoContracts {
		body, _ := gen.renderContractsPage(ctx)
		w.Write(filepath.ToSlash(filepath.Join(repoSlug, "contracts", "api-surface.md")), []byte(body))
	}
	hs, _ := gen.renderHotspotsPage(ctx)
	w.Write(filepath.ToSlash(filepath.Join(repoSlug, "analysis", "hotspots.md")), []byte(hs))
	cy, _ := gen.renderCyclesPage(ctx)
	w.Write(filepath.ToSlash(filepath.Join(repoSlug, "analysis", "cycles.md")), []byte(cy))
	sm, _ := gen.renderSemanticPage(ctx)
	w.Write(filepath.ToSlash(filepath.Join(repoSlug, "analysis", "semantic.md")), []byte(sm))
	w.Write(filepath.ToSlash(filepath.Join(repoSlug, "_assets", "community-graph.mermaid")),
		[]byte(RenderCommunityGraph(gen.graph, gen.communities,
			CommunityGraphOpts{MinSize: gen.opts.MinCommunity, Max: gen.opts.MaxCommunities})))
	w.Write(filepath.ToSlash(filepath.Join("_workspace", "README.md")),
		[]byte("# Workspace pages\n\nReserved for cross-repo wiki pages. Single-repo mode leaves this directory empty.\n"))
	if !gen.opts.NoDocs && gen.docsBundle != "" {
		w.Write(filepath.ToSlash(filepath.Join(repoSlug, "changelog.md")), []byte(gen.docsBundle))
	}
	return w.Files()
}
