package graphbuilder

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

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/semantic"
)

func TestRunDomainRequiresValidKnowledgeGraphBeforeInvocation(t *testing.T) {
	root := t.TempDir()
	runner := &recordingSemanticRunner{output: validDomainGraphJSON(t)}
	_, err := RunDomain(context.Background(), Options{InputRepo: root, OutputRoot: root, SemanticRunner: runner}, RunCommand)
	require.ErrorContains(t, err, "knowledge-graph.json is required")
	require.Empty(t, runner.calls)

	require.NoError(t, os.MkdirAll(filepath.Join(root, ".agentteam-insight"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(root, ".agentteam-insight", "knowledge-graph.json"), []byte(`{"nodes":{}}`), 0o644))
	_, err = RunDomain(context.Background(), Options{InputRepo: root, OutputRoot: root, SemanticRunner: runner}, RunCommand)
	require.ErrorContains(t, err, "invalid knowledge graph")
	require.Empty(t, runner.calls)
}

func TestRunDomainInvokesPinnedAnalyzerOnceAndSavesGraph(t *testing.T) {
	root := writeKnowledgeGraph(t)
	runner := &recordingSemanticRunner{output: validDomainGraphJSON(t)}
	var progress bytes.Buffer
	result, err := RunDomain(context.Background(), Options{InputRepo: root, OutputRoot: root, SemanticRunner: runner, ProgressWriter: &progress}, RunCommand)
	require.NoError(t, err)
	require.Len(t, runner.calls, 1)
	call := runner.calls[0]
	require.Equal(t, semantic.Domain, call.Stage)
	require.Equal(t, 1, call.Attempt)
	require.Equal(t, ".ua/intermediate/domain-analysis.json", call.OutputFile)
	require.Contains(t, call.Prompt, "# Domain Analyzer Agent")
	require.Contains(t, call.Prompt, "Input mode: existing knowledge graph")
	require.NotContains(t, call.Prompt, `file:main.go`)
	require.Equal(t, ".ua/intermediate/knowledge-graph.json", call.InputFile)
	require.Contains(t, string(call.Input), `"id": "file:main.go"`)
	require.Contains(t, strings.ToLower(call.Prompt), "without reading source files")
	require.Contains(t, strings.ToLower(call.Prompt), "do not read source files")
	require.Equal(t, filepath.Join(root, ".agentteam-insight", "domain-graph.json"), result.GraphPath)
	data, err := os.ReadFile(result.GraphPath)
	require.NoError(t, err)
	validated := schema.ValidateGraphJSON(data)
	require.True(t, validated.Success, validated.Fatal)
	require.NoError(t, schema.ValidateDomainGraph(*validated.Data))
	require.Empty(t, validated.Data.Layers)
	require.Empty(t, validated.Data.Tour)
	require.Equal(t, []string{
		"loading knowledge graph: " + filepath.Join(root, ".agentteam-insight", "knowledge-graph.json"),
		"knowledge graph validated: nodes=1 edges=0",
		"starting domain analysis with one LLM invocation",
		"domain analysis completed; validating generated graph",
		"domain graph validated: nodes=3 edges=2",
		"writing domain graph: " + result.GraphPath,
		"domain graph written: " + result.GraphPath,
	}, progressLines(progress.String()))
}

func progressLines(output string) []string {
	lines := strings.Split(strings.TrimSpace(output), "\n")
	for index := range lines {
		lines[index] = strings.TrimPrefix(lines[index], "[graph-builder] ")
	}
	return lines
}

func TestRunDomainKeepsLargeKnowledgeGraphOutOfCommandArguments(t *testing.T) {
	root := writeKnowledgeGraph(t)
	path := filepath.Join(root, ".agentteam-insight", "knowledge-graph.json")
	data, err := os.ReadFile(path)
	require.NoError(t, err)
	largeMarker := strings.Repeat("large-context-marker-", 100_000)
	data = []byte(strings.Replace(string(data), `"Demo"`, strconv.Quote(largeMarker), 1))
	require.NoError(t, os.WriteFile(path, data, 0o644))

	var command Command
	_, err = RunDomain(context.Background(), Options{InputRepo: root, OutputRoot: root}, func(_ context.Context, request Command) (CommandResult, error) {
		command = request
		input, readErr := os.ReadFile(filepath.Join(request.Dir, ".ua", "intermediate", "knowledge-graph.json"))
		require.NoError(t, readErr)
		require.Contains(t, string(input), largeMarker)
		return CommandResult{Stdout: validDomainGraphJSON(t)}, nil
	})
	require.NoError(t, err)
	for _, argument := range command.Args {
		require.NotContains(t, argument, largeMarker)
		require.Less(t, len(argument), 20_000)
	}
}

func TestRunDomainInvalidOutputDoesNotOverwriteExistingGraph(t *testing.T) {
	root := writeKnowledgeGraph(t)
	path := filepath.Join(root, ".agentteam-insight", "domain-graph.json")
	require.NoError(t, os.WriteFile(path, []byte("previous\n"), 0o644))
	runner := &recordingSemanticRunner{output: []byte(`{"version":"1.0.0","project":{"languages":[],"frameworks":[]},"nodes":[],"edges":[],"layers":[],"tour":[]}`)}
	_, err := RunDomain(context.Background(), Options{InputRepo: root, OutputRoot: root, SemanticRunner: runner}, RunCommand)
	require.ErrorContains(t, err, "validate domain graph")
	require.Len(t, runner.calls, 1)
	data, readErr := os.ReadFile(path)
	require.NoError(t, readErr)
	require.Equal(t, "previous\n", string(data))
}

type recordingSemanticRunner struct {
	output []byte
	calls  []semantic.Invocation
}

func (runner *recordingSemanticRunner) Invoke(_ context.Context, invocation semantic.Invocation) ([]byte, error) {
	runner.calls = append(runner.calls, invocation)
	return append([]byte(nil), runner.output...), nil
}

func writeKnowledgeGraph(t *testing.T) string {
	t.Helper()
	root := t.TempDir()
	dir := filepath.Join(root, ".agentteam-insight")
	require.NoError(t, os.MkdirAll(dir, 0o755))
	graph := schema.KnowledgeGraph{
		Version: "1.0.0", Project: schema.ProjectMeta{Name: "demo", Languages: []string{"go"}, Frameworks: []string{}, Description: "Demo", AnalyzedAt: "now", GitCommitHash: "abc"},
		Nodes: []schema.GraphNode{{ID: "file:main.go", Type: "file", Name: "main.go", FilePath: "main.go", Summary: "Starts the service", Tags: []string{"entrypoint"}, Complexity: "simple"}},
		Edges: []schema.GraphEdge{}, Layers: []schema.Layer{{ID: "layer:app", Name: "App", Description: "App", NodeIDs: []string{"file:main.go"}}}, Tour: []schema.TourStep{},
	}
	data, err := json.Marshal(graph)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(dir, "knowledge-graph.json"), data, 0o644))
	return root
}

func validDomainGraphJSON(t *testing.T) []byte {
	t.Helper()
	graph := schema.KnowledgeGraph{
		Version: "1.0.0", Project: schema.ProjectMeta{Name: "demo", Languages: []string{"go"}, Frameworks: []string{}, Description: "Demo", AnalyzedAt: "now", GitCommitHash: "abc"},
		Nodes: []schema.GraphNode{
			{ID: "domain:service", Type: "domain", Name: "Service", Summary: "Owns service behavior", Tags: []string{"service"}, Complexity: "simple"},
			{ID: "flow:start", Type: "flow", Name: "Start", Summary: "Starts the service", Tags: []string{"startup"}, Complexity: "simple"},
			{ID: "step:start:load", Type: "step", Name: "Load", Summary: "Loads configuration", Tags: []string{"config"}, Complexity: "simple", FilePath: "main.go", LineRange: []int{1, 2}},
		},
		Edges: []schema.GraphEdge{
			{Source: "domain:service", Target: "flow:start", Type: "contains_flow", Direction: "forward", Weight: 1},
			{Source: "flow:start", Target: "step:start:load", Type: "flow_step", Direction: "forward", Weight: 0.1},
		}, Layers: []schema.Layer{}, Tour: []schema.TourStep{},
	}
	data, err := json.Marshal(graph)
	require.NoError(t, err)
	require.False(t, strings.Contains(string(data), "source code"))
	return data
}
