package main

import (
	"bytes"
	"context"
	"encoding/json"
	"flag"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/codewiki"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder"
)

func TestRunRequiresSubcommand(t *testing.T) {
	err := run(context.Background(), nil, &bytes.Buffer{}, &bytes.Buffer{}, commandRunners{})
	if err == nil {
		t.Fatal("expected missing subcommand error")
	}
	if !strings.Contains(err.Error(), "subcommand is required") {
		t.Fatalf("expected missing subcommand error, got %v", err)
	}
}

func TestParseDomainOptionsKeepsExistingGraphPathMinimal(t *testing.T) {
	options, err := parseDomainOptions([]string{"--input", "/repo", "--model", "provider/model", "--output-language", "zh-CN"}, io.Discard)
	require.NoError(t, err)
	require.Equal(t, "/repo", options.InputRepo)
	require.Equal(t, "provider/model", options.OpenCodeModel)
	require.Equal(t, "zh-CN", options.OutputLanguage)

	_, err = parseDomainOptions([]string{"--input", "/repo", "--full"}, io.Discard)
	require.ErrorContains(t, err, "flag provided but not defined")
}

func TestRunRejectsUnknownSubcommand(t *testing.T) {
	err := run(context.Background(), []string{"unknown"}, &bytes.Buffer{}, &bytes.Buffer{}, commandRunners{})
	if err == nil {
		t.Fatal("expected unknown subcommand error")
	}
	if !strings.Contains(err.Error(), "unknown subcommand") {
		t.Fatalf("expected unknown subcommand error, got %v", err)
	}
}

func TestRunHelpListsPublicCommands(t *testing.T) {
	var stderr bytes.Buffer
	err := run(context.Background(), []string{"--help"}, &bytes.Buffer{}, &stderr, commandRunners{})

	require.ErrorIs(t, err, flag.ErrHelp)
	require.Contains(t, stderr.String(), "agentteam-insight graph")
	require.Contains(t, stderr.String(), "agentteam-insight domain")
	require.Contains(t, stderr.String(), "agentteam-insight wiki")
	require.NotContains(t, stderr.String(), "analyze")
	require.NotContains(t, stderr.String(), legacyGraphBuilderCommand())
}

func TestRunRejectsAnalyzeSubcommand(t *testing.T) {
	legacyCommand := "analyze"
	var stderr bytes.Buffer
	err := run(context.Background(), []string{legacyCommand}, &bytes.Buffer{}, &stderr, commandRunners{})

	require.Error(t, err)
	require.Contains(t, err.Error(), `unknown subcommand "`+legacyCommand+`"`)
	require.NotContains(t, stderr.String(), "agentteam-insight "+legacyCommand)
	require.NotContains(t, stderr.String(), legacyBinaryName())
}

func TestRunRejectsLegacyGraphBuilderSubcommand(t *testing.T) {
	legacyCommand := legacyGraphBuilderCommand()
	var stderr bytes.Buffer
	err := run(context.Background(), []string{legacyCommand}, &bytes.Buffer{}, &stderr, commandRunners{})

	require.Error(t, err)
	require.Contains(t, err.Error(), `unknown subcommand "`+legacyCommand+`"`)
	require.NotContains(t, stderr.String(), "agentteam-insight "+legacyCommand)
	require.NotContains(t, stderr.String(), legacyBinaryName())
}

func TestRunGraphHelpUsesGraphBuilderTerminology(t *testing.T) {
	var stderr bytes.Buffer
	err := run(context.Background(), []string{"graph", "--help"}, &bytes.Buffer{}, &stderr, commandRunners{})

	require.ErrorIs(t, err, flag.ErrHelp)
	require.Contains(t, stderr.String(), "Usage: agentteam-insight graph")
	require.Contains(t, stderr.String(), "graph builder")
	require.NotContains(t, stderr.String(), "agentteam-insight "+legacyGraphBuilderCommand())
	require.NotContains(t, stderr.String(), legacyBinaryName())
}

func legacyBinaryName() string { return "agentteam" + "-code" }

func legacyGraphBuilderCommand() string { return "under" + "stand" }

func TestRunWikiPassesWikiOptions(t *testing.T) {
	inputRootA := filepath.Join(t.TempDir(), "repo-a.kb")
	inputRootB := filepath.Join(t.TempDir(), "repo-b.kb")
	outputRoot := t.TempDir()
	require.NoError(t, os.MkdirAll(inputRootA, 0o755))
	require.NoError(t, os.MkdirAll(inputRootB, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(inputRootA, "README.md"), []byte("# Repo A\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(inputRootB, "README.md"), []byte("# Repo B\n"), 0o644))

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	err := run(
		context.Background(),
		[]string{
			"wiki",
			"--input", inputRootA,
			"--input", inputRootB,
			"--output", outputRoot,
			"--jobs", "2",
			"--opencode", "custom-opencode",
			"--opencode-agent", "writer",
			"--model", "provider/model",
			"--isolate-opencode-runtime",
			"--task-timeout", "1m",
			"--output-language", "Simplified Chinese",
			"--opencode-arg", "--log-level",
			"--opencode-arg", "INFO",
		},
		&stdout,
		&stderr,
		commandRunners{
			wiki: func(ctx context.Context, command codewiki.Command) (codewiki.CommandResult, error) {
				deadline, ok := ctx.Deadline()
				require.True(t, ok)
				require.WithinDuration(t, time.Now().Add(time.Minute), deadline, 5*time.Second)
				require.Equal(t, "custom-opencode", command.Name)
				require.Contains(t, command.Args, "--agent")
				require.Contains(t, command.Args, "writer")
				require.Contains(t, command.Args, "--model")
				require.Contains(t, command.Args, "provider/model")
				require.NotContains(t, command.Args, "--dangerously-skip-permissions")
				require.Contains(t, command.Args, "--log-level")
				require.NotEmpty(t, envValue(command.Env, "XDG_DATA_HOME"))
				require.NotEmpty(t, envValue(command.Env, "XDG_STATE_HOME"))

				if filepath.Base(command.Dir) == "planner" {
					require.NoFileExists(t, filepath.Join(command.Dir, "wiki_plan_task.json"))
					prompt := strings.Join(command.Args, "\n")
					require.Contains(t, prompt, "- repo-a: "+inputRootA)
					require.Contains(t, prompt, "- repo-b: "+inputRootB)
					require.Contains(t, prompt, "- "+outputRoot)
					require.Contains(t, prompt, "Output language: Write all generated natural-language content in Simplified Chinese.")
					writeCLIWikiPlan(t, command.Dir, codewiki.WikiPlan{
						Schema: codewiki.WikiPlanSchema,
						Homepage: codewiki.WikiPage{
							ID:      "home",
							Path:    "README.md",
							Title:   "Home",
							Summary: "Overview.",
						},
					})
					return codewiki.CommandResult{}, nil
				}
				require.NoFileExists(t, filepath.Join(command.Dir, "wiki_page_task.json"))
				pagePrompt := strings.Join(command.Args, "\n")
				require.Contains(t, pagePrompt, "- path: README.md")
				require.Contains(t, pagePrompt, "Output language: Write all generated natural-language content in Simplified Chinese.")
				require.NoError(t, os.WriteFile(filepath.Join(outputRoot, "README.md"), []byte("# Wiki\n"), 0o644))
				return codewiki.CommandResult{}, nil
			},
		},
	)

	require.NoError(t, err)
	require.NotContains(t, stderr.String(), "discovering wiki input files")
	require.Contains(t, stdout.String(), "generated code wiki")
	require.NotContains(t, stdout.String(), "input files")
	require.Contains(t, stdout.String(), "2 OpenCode tasks")
	require.Contains(t, stdout.String(), outputRoot)
	require.FileExists(t, filepath.Join(outputRoot, "README.md"))
}

func TestRunGraphPreparesGraphOutput(t *testing.T) {
	inputRepo := t.TempDir()
	outputRoot := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(inputRepo, "main.go"), []byte("package main\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(outputRoot, ".agentteam-insight"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(outputRoot, ".agentteam-insightignore"), []byte("# accepted\n"), 0o644))

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	var runnerCalled bool
	err := run(
		context.Background(),
		[]string{
			"graph",
			"--input", inputRepo,
			"--output", outputRoot,
			"--mode", "full",
			"--llm", "off",
			"--no-prompt",
			"--jobs", "3",
			"--output-language", "Simplified Chinese",
		},
		&stdout,
		&stderr,
		commandRunners{
			graph: func(context.Context, graphbuilder.Command) (graphbuilder.CommandResult, error) {
				runnerCalled = true
				return graphbuilder.CommandResult{}, nil
			},
		},
	)

	require.NoError(t, err)
	require.False(t, runnerCalled, "deterministic graph path must not invoke OpenCode by default")
	require.Contains(t, stderr.String(), "[graph-builder] starting full graph run")
	require.Contains(t, stderr.String(), "saving full graph artifacts")
	require.Contains(t, stdout.String(), "graph builder FULL_UPDATE in full mode")
	require.Contains(t, stdout.String(), "analyzed 1 files")
	require.Contains(t, stdout.String(), "nodes=1")
	require.Contains(t, stdout.String(), filepath.Join(outputRoot, ".agentteam-insight", "knowledge-graph.json"))
}

func TestRunGraphOpenCodeAgentEnablesFileAnalyzerRunner(t *testing.T) {
	inputRepo := t.TempDir()
	outputRoot := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(inputRepo, "main.go"), []byte("package main\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(outputRoot, ".agentteam-insight"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(outputRoot, ".agentteam-insightignore"), []byte("# accepted\n"), 0o644))

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	var runnerCalled bool
	err := run(
		context.Background(),
		[]string{
			"graph",
			"--input", inputRepo,
			"--output", outputRoot,
			"--mode", "full",
			"--no-prompt",
			"--opencode", "custom-opencode",
			"--opencode-agent", "file-analyzer",
			"--model", "provider/model",
			"--output-language", "Chinese",
		},
		&stdout,
		&stderr,
		commandRunners{
			graph: func(ctx context.Context, command graphbuilder.Command) (graphbuilder.CommandResult, error) {
				runnerCalled = true
				if _, err := os.Stat(filepath.Join(command.Dir, "task.json")); os.IsNotExist(err) {
					return graphbuilder.CommandResult{Stdout: []byte(`[]`)}, nil
				}
				deadline, ok := ctx.Deadline()
				require.True(t, ok)
				require.WithinDuration(t, time.Now().Add(30*time.Minute), deadline, 5*time.Second)
				require.Equal(t, "custom-opencode", command.Name)
				require.Contains(t, command.Args, "--agent")
				require.Contains(t, command.Args, "file-analyzer")
				require.Contains(t, command.Args, "--model")
				require.Contains(t, command.Args, "provider/model")
				prompt := strings.Join(command.Args, "\n")
				require.Contains(t, prompt, "request.batchIndex")
				require.Contains(t, prompt, "request.project")
				var manifest map[string]any
				data, err := os.ReadFile(filepath.Join(command.Dir, "task.json"))
				require.NoError(t, err)
				require.NoError(t, json.Unmarshal(data, &manifest))
				require.Equal(t, "agentteam-insight.graph.file-analyzer-task.v2", manifest["schema"])
				request := manifest["request"].(map[string]any)
				require.NotEmpty(t, request["batchIndex"])
				require.NotEmpty(t, request["files"])
				project := request["project"].(map[string]any)
				require.NotEmpty(t, project["name"])
				require.Contains(t, project["supportedImportResolvers"], "go")
				outputPath := manifest["outputPath"].(string)
				evidence := manifest["evidence"].(map[string]any)
				patchNodes := []map[string]any{}
				for _, raw := range evidence["nodes"].([]any) {
					node := raw.(map[string]any)
					patchNodes = append(patchNodes, map[string]any{
						"id": node["id"], "summary": "Explains the responsibility of " + node["name"].(string), "tags": []string{"responsibility"},
					})
				}
				graphBytes, err := json.Marshal(map[string]any{"schema": "agentteam-insight.graph.file-analyzer-patch.v1", "nodes": patchNodes})
				require.NoError(t, err)
				require.NoError(t, os.WriteFile(outputPath, graphBytes, 0o644))
				return graphbuilder.CommandResult{}, nil
			},
		},
	)

	require.NoError(t, err)
	require.True(t, runnerCalled)
	require.Contains(t, stdout.String(), "graph builder FULL_UPDATE in full mode")
}

func TestRunGraphDefaultsOutputAndMode(t *testing.T) {
	inputRepo := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(inputRepo, "main.go"), []byte("package main\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(inputRepo, ".agentteam-insight"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(inputRepo, ".agentteam-insightignore"), []byte("# accepted\n"), 0o644))

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	err := run(
		context.Background(),
		[]string{"graph", "--input", inputRepo, "--no-prompt"},
		&stdout,
		&stderr,
		commandRunners{graph: groundedGraphCommandRunner},
	)

	require.NoError(t, err)
	require.Contains(t, stdout.String(), "graph builder FULL_UPDATE in auto mode")
	require.Contains(t, stdout.String(), filepath.Join(inputRepo, ".agentteam-insight", "knowledge-graph.json"))
}

func TestRunGraphRejectsInvalidLLMMode(t *testing.T) {
	inputRepo := t.TempDir()
	err := run(
		context.Background(),
		[]string{"graph", "--input", inputRepo, "--llm", "tour"},
		&bytes.Buffer{},
		&bytes.Buffer{},
		commandRunners{},
	)

	require.Error(t, err)
	require.Contains(t, err.Error(), "--llm must be one of auto or off")
}

func TestRunGraphRejectsInvalidMode(t *testing.T) {
	inputRepo := t.TempDir()
	err := run(
		context.Background(),
		[]string{"graph", "--input", inputRepo, "--mode", "sometimes"},
		&bytes.Buffer{},
		&bytes.Buffer{},
		commandRunners{},
	)

	require.Error(t, err)
	require.Contains(t, err.Error(), "--mode must be one of full, incremental, or auto")
}

func TestRunGraphLLMOffAllowsParallelDeterministicRun(t *testing.T) {
	inputRepo := t.TempDir()
	options, err := parseGraphOptions([]string{"--input", inputRepo, "--jobs", "2", "--model", "provider/model", "--llm", "off"}, &bytes.Buffer{})
	require.NoError(t, err)
	require.Equal(t, graphbuilder.LLMModeOff, options.LLMMode)
}

func TestParseGraphOptionsDefaultsToAutoLLM(t *testing.T) {
	options, err := parseGraphOptions([]string{"--input", t.TempDir()}, &bytes.Buffer{})
	require.NoError(t, err)
	require.Equal(t, graphbuilder.LLMModeAuto, options.LLMMode)
}

func TestParseGraphOptionsCreatesBenchmarkReportWriter(t *testing.T) {
	path := filepath.Join(t.TempDir(), "benchmark.json")
	options, err := parseGraphOptions([]string{"--input", t.TempDir(), "--benchmark", path}, &bytes.Buffer{})
	require.NoError(t, err)
	require.Equal(t, path, options.BenchmarkPath)
}

func TestRunGraphJobsWithAgentEnhancementUsesAutomaticIsolation(t *testing.T) {
	inputRepo := t.TempDir()
	options, err := parseGraphOptions([]string{"--input", inputRepo, "--jobs", "2", "--model", "provider/model"}, &bytes.Buffer{})
	require.NoError(t, err)
	require.Equal(t, 2, options.Jobs)
}

func TestRunGraphJobsWithDefaultAutoLLMUsesAutomaticIsolation(t *testing.T) {
	options, err := parseGraphOptions([]string{"--input", t.TempDir(), "--jobs", "2"}, &bytes.Buffer{})
	require.NoError(t, err)
	require.Equal(t, 2, options.Jobs)
}

func envValue(env []string, key string) string {
	prefix := key + "="
	for _, entry := range env {
		if strings.HasPrefix(entry, prefix) {
			return strings.TrimPrefix(entry, prefix)
		}
	}
	return ""
}

func groundedGraphCommandRunner(_ context.Context, command graphbuilder.Command) (graphbuilder.CommandResult, error) {
	data, err := os.ReadFile(filepath.Join(command.Dir, "task.json"))
	if err != nil {
		return graphbuilder.CommandResult{Stdout: []byte(`[]`)}, nil
	}
	var manifest map[string]any
	if err := json.Unmarshal(data, &manifest); err != nil {
		return graphbuilder.CommandResult{}, err
	}
	evidence := manifest["evidence"].(map[string]any)
	patchNodes := []map[string]any{}
	for _, raw := range evidence["nodes"].([]any) {
		node := raw.(map[string]any)
		patchNodes = append(patchNodes, map[string]any{
			"id": node["id"], "summary": "Explains the responsibility of " + node["name"].(string), "tags": []string{"responsibility"},
		})
	}
	payload, err := json.Marshal(map[string]any{"schema": "agentteam-insight.graph.file-analyzer-patch.v1", "nodes": patchNodes})
	if err != nil {
		return graphbuilder.CommandResult{}, err
	}
	return graphbuilder.CommandResult{}, os.WriteFile(manifest["outputPath"].(string), payload, 0o644)
}

func writeCLIWikiPlan(t *testing.T, workDir string, plan codewiki.WikiPlan) {
	t.Helper()
	data, err := json.MarshalIndent(plan, "", "  ")
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "wiki_plan.json"), append(data, '\n'), 0o644))
}

func TestRunGraphPersistsPreflightFlags(t *testing.T) {
	inputRepo := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(inputRepo, "main.go"), []byte("package main\n"), 0o644))
	require.NoError(t, os.MkdirAll(filepath.Join(inputRepo, ".agentteam-insight"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(inputRepo, ".agentteam-insightignore"), []byte("# accepted\n"), 0o644))

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	err := run(
		context.Background(),
		[]string{"graph", "--input", inputRepo, "--mode", "full", "--no-prompt", "--auto-update", "--language", "japanese"},
		&stdout,
		&stderr,
		commandRunners{graph: groundedGraphCommandRunner},
	)

	require.NoError(t, err)
	data, err := os.ReadFile(filepath.Join(inputRepo, ".agentteam-insight", "config.json"))
	require.NoError(t, err)
	require.Contains(t, string(data), `"autoUpdate": true`)
	require.Contains(t, string(data), `"outputLanguage": "ja"`)
	require.Contains(t, stdout.String(), "graph builder FULL_UPDATE")
}

func TestRunGraphRejectsConflictingAutoUpdateFlags(t *testing.T) {
	inputRepo := t.TempDir()
	err := run(
		context.Background(),
		[]string{"graph", "--input", inputRepo, "--auto-update", "--no-auto-update"},
		&bytes.Buffer{},
		&bytes.Buffer{},
		commandRunners{},
	)

	require.Error(t, err)
	require.Contains(t, err.Error(), "--auto-update and --no-auto-update are mutually exclusive")
}

func TestRunGraphRejectsConflictingLanguageFlags(t *testing.T) {
	inputRepo := t.TempDir()
	err := run(
		context.Background(),
		[]string{"graph", "--input", inputRepo, "--language", "zh", "--output-language", "en"},
		&bytes.Buffer{},
		&bytes.Buffer{},
		commandRunners{},
	)

	require.Error(t, err)
	require.Contains(t, err.Error(), "--language and --output-language must not conflict")
}
