package cli

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

	agginventory "github.com/Clyra-AI/wrkr/core/aggregate/inventory"
	"github.com/Clyra-AI/wrkr/core/config"
	githubpr "github.com/Clyra-AI/wrkr/core/github/pr"
	"github.com/Clyra-AI/wrkr/core/manifest"
	"github.com/Clyra-AI/wrkr/core/model"
	"github.com/Clyra-AI/wrkr/core/risk"
	"github.com/Clyra-AI/wrkr/core/source"
	"github.com/Clyra-AI/wrkr/core/state"
)

func TestFixJSONTopThreeDeterministic(t *testing.T) {
	t.Parallel()

	statePath := writeFixStateFixture(t)

	var outA bytes.Buffer
	var errA bytes.Buffer
	code := Run([]string{"fix", "--state", statePath, "--top", "3", "--json"}, &outA, &errA)
	if code != 0 {
		t.Fatalf("fix run A failed: %d stderr=%q", code, errA.String())
	}
	if errA.Len() != 0 {
		t.Fatalf("expected empty stderr, got %q", errA.String())
	}

	var outB bytes.Buffer
	var errB bytes.Buffer
	code = Run([]string{"fix", "--state", statePath, "--top", "3", "--json"}, &outB, &errB)
	if code != 0 {
		t.Fatalf("fix run B failed: %d stderr=%q", code, errB.String())
	}
	if outA.String() != outB.String() {
		t.Fatalf("expected deterministic output\nA=%s\nB=%s", outA.String(), outB.String())
	}

	var payload map[string]any
	if err := json.Unmarshal(outA.Bytes(), &payload); err != nil {
		t.Fatalf("parse fix payload: %v", err)
	}
	if payload["remediation_count"] != float64(3) {
		t.Fatalf("expected remediation_count=3, got %v", payload["remediation_count"])
	}
}

func TestFixOpenPRFailsClosedWithScanOnlyToken(t *testing.T) {
	t.Parallel()

	tmp := t.TempDir()
	statePath := writeFixStateFixture(t)
	configPath := filepath.Join(tmp, "config.json")

	cfg := config.Default()
	cfg.DefaultTarget = config.Target{Mode: config.TargetRepo, Value: "acme/backend"}
	cfg.Auth.Scan.Token = "scan-only-token"
	if err := config.Save(configPath, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}

	var out bytes.Buffer
	var errOut bytes.Buffer
	code := Run([]string{"fix", "--state", statePath, "--config", configPath, "--open-pr", "--repo", "acme/backend", "--json"}, &out, &errOut)
	if code != 4 {
		t.Fatalf("expected exit 4, got %d stdout=%q stderr=%q", code, out.String(), errOut.String())
	}

	var payload map[string]any
	if err := json.Unmarshal(errOut.Bytes(), &payload); err != nil {
		t.Fatalf("parse error payload: %v", err)
	}
	errorObj, ok := payload["error"].(map[string]any)
	if !ok {
		t.Fatalf("expected error payload, got %v", payload)
	}
	if errorObj["code"] != "approval_required" {
		t.Fatalf("expected approval_required code, got %v", errorObj["code"])
	}
}

func TestFixOpenPRWritesRemediationArtifacts(t *testing.T) {
	tmp := t.TempDir()
	statePath := writeFixStateFixture(t)
	configPath := filepath.Join(tmp, "config.json")

	cfg := config.Default()
	cfg.DefaultTarget = config.Target{Mode: config.TargetRepo, Value: "acme/backend"}
	cfg.Auth.Scan.Token = "scan-read-token"
	cfg.Auth.Fix.Token = "fix-write-token"
	if err := config.Save(configPath, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}

	stub := &stubPRAPI{}
	previousClient := newGitHubPRClient
	newGitHubPRClient = func(_, _ string) githubpr.API { return stub }
	t.Cleanup(func() { newGitHubPRClient = previousClient })

	var out bytes.Buffer
	var errOut bytes.Buffer
	code := Run([]string{"fix", "--state", statePath, "--config", configPath, "--open-pr", "--repo", "acme/backend", "--json"}, &out, &errOut)
	if code != 0 {
		t.Fatalf("expected success, got %d stdout=%q stderr=%q", code, out.String(), errOut.String())
	}
	if errOut.Len() != 0 {
		t.Fatalf("expected empty stderr, got %q", errOut.String())
	}

	var payload map[string]any
	if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
		t.Fatalf("parse payload: %v", err)
	}
	artifacts, ok := payload["remediation_artifacts"].(map[string]any)
	if !ok {
		t.Fatalf("expected remediation_artifacts, got %v", payload)
	}
	if got := int(artifacts["changed_count"].(float64)); got <= 0 {
		t.Fatalf("expected changed_count > 0, got %d", got)
	}
	if got := stub.ensureFileCalls; got <= 0 {
		t.Fatalf("expected EnsureFileContent calls, got %d", got)
	}
	for _, item := range stub.paths {
		if strings.Contains(item, `\`) {
			t.Fatalf("expected POSIX remediation artifact paths, got %q", item)
		}
	}
	artifactPaths, ok := artifacts["paths"].([]any)
	if !ok || len(artifactPaths) == 0 {
		t.Fatalf("expected remediation_artifacts.paths array, got %v", artifacts["paths"])
	}
	for _, item := range artifactPaths {
		pathValue, ok := item.(string)
		if !ok {
			t.Fatalf("expected path string, got %T", item)
		}
		if strings.Contains(pathValue, `\`) {
			t.Fatalf("expected POSIX remediation_artifacts.paths entry, got %q", pathValue)
		}
	}
	if _, ok := payload["pull_request"].(map[string]any); !ok {
		t.Fatalf("expected pull_request payload, got %v", payload["pull_request"])
	}
}

func TestFixApplyRequiresOpenPR(t *testing.T) {
	statePath := writeApplyStateFixture(t)
	var out bytes.Buffer
	var errOut bytes.Buffer
	code := Run([]string{"fix", "--state", statePath, "--apply", "--json"}, &out, &errOut)
	if code != 6 {
		t.Fatalf("expected exit 6, got %d stdout=%q stderr=%q", code, out.String(), errOut.String())
	}
}

func TestFixApplyFailsClosedWhenNoApplyCapableRemediations(t *testing.T) {
	tmp := t.TempDir()
	statePath := writeFixStateFixture(t)
	configPath := filepath.Join(tmp, "config.json")

	cfg := config.Default()
	cfg.DefaultTarget = config.Target{Mode: config.TargetRepo, Value: "acme/backend"}
	cfg.Auth.Scan.Token = "scan-read-token"
	cfg.Auth.Fix.Token = "fix-write-token"
	if err := config.Save(configPath, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}

	stub := &stubPRAPI{}
	previousClient := newGitHubPRClient
	newGitHubPRClient = func(_, _ string) githubpr.API { return stub }
	t.Cleanup(func() { newGitHubPRClient = previousClient })

	var out bytes.Buffer
	var errOut bytes.Buffer
	code := Run([]string{"fix", "--state", statePath, "--config", configPath, "--apply", "--open-pr", "--repo", "acme/backend", "--json"}, &out, &errOut)
	if code != 8 {
		t.Fatalf("expected exit 8, got %d stdout=%q stderr=%q", code, out.String(), errOut.String())
	}
}

func TestFixApplyOpenPRWritesSupportedRepoFile(t *testing.T) {
	tmp := t.TempDir()
	statePath := writeApplyStateFixture(t)
	configPath := filepath.Join(tmp, "config.json")

	cfg := config.Default()
	cfg.DefaultTarget = config.Target{Mode: config.TargetRepo, Value: "acme/backend"}
	cfg.Auth.Scan.Token = "scan-read-token"
	cfg.Auth.Fix.Token = "fix-write-token"
	if err := config.Save(configPath, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}

	stub := &stubPRAPI{}
	previousClient := newGitHubPRClient
	newGitHubPRClient = func(_, _ string) githubpr.API { return stub }
	t.Cleanup(func() { newGitHubPRClient = previousClient })

	var out bytes.Buffer
	var errOut bytes.Buffer
	code := Run([]string{"fix", "--state", statePath, "--config", configPath, "--apply", "--open-pr", "--repo", "acme/backend", "--json"}, &out, &errOut)
	if code != 0 {
		t.Fatalf("expected success, got %d stdout=%q stderr=%q", code, out.String(), errOut.String())
	}

	var payload map[string]any
	if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
		t.Fatalf("parse payload: %v", err)
	}
	if payload["mode"] != "apply" {
		t.Fatalf("expected mode=apply, got %v", payload["mode"])
	}
	if payload["apply_supported_count"] != float64(1) {
		t.Fatalf("expected apply_supported_count=1, got %v", payload["apply_supported_count"])
	}
	artifacts, ok := payload["remediation_artifacts"].(map[string]any)
	if !ok {
		t.Fatalf("expected remediation_artifacts, got %v", payload["remediation_artifacts"])
	}
	paths, ok := artifacts["paths"].([]any)
	if !ok {
		t.Fatalf("expected artifact paths array, got %T", artifacts["paths"])
	}
	foundManifest := false
	for _, item := range paths {
		if item.(string) == ".wrkr/wrkr-manifest.yaml" {
			foundManifest = true
		}
	}
	if !foundManifest {
		t.Fatalf("expected apply artifacts to include .wrkr/wrkr-manifest.yaml, got %v", paths)
	}
}

func TestFixOpenPRMaxPRsPublishesMultiplePullRequests(t *testing.T) {
	tmp := t.TempDir()
	statePath := writeFixStateFixture(t)
	configPath := filepath.Join(tmp, "config.json")

	cfg := config.Default()
	cfg.DefaultTarget = config.Target{Mode: config.TargetRepo, Value: "acme/backend"}
	cfg.Auth.Scan.Token = "scan-read-token"
	cfg.Auth.Fix.Token = "fix-write-token"
	if err := config.Save(configPath, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}

	stub := &stubPRAPI{}
	previousClient := newGitHubPRClient
	newGitHubPRClient = func(_, _ string) githubpr.API { return stub }
	t.Cleanup(func() { newGitHubPRClient = previousClient })

	var out bytes.Buffer
	var errOut bytes.Buffer
	code := Run([]string{"fix", "--state", statePath, "--config", configPath, "--open-pr", "--max-prs", "2", "--repo", "acme/backend", "--json"}, &out, &errOut)
	if code != 0 {
		t.Fatalf("expected success, got %d stdout=%q stderr=%q", code, out.String(), errOut.String())
	}

	var payload map[string]any
	if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
		t.Fatalf("parse payload: %v", err)
	}
	prs, ok := payload["pull_requests"].([]any)
	if !ok || len(prs) != 2 {
		t.Fatalf("expected two pull_requests, got %v", payload["pull_requests"])
	}
	if stub.createCalls != 2 {
		t.Fatalf("expected two PR creates, got %d", stub.createCalls)
	}
}

func TestFixHelpMatchesBehaviorContract(t *testing.T) {
	t.Parallel()

	var out bytes.Buffer
	var errOut bytes.Buffer
	code := Run([]string{"fix", "--help"}, &out, &errOut)
	if code != 0 {
		t.Fatalf("expected exit 0, got %d stderr=%q", code, errOut.String())
	}

	helpText := errOut.String()
	for _, sentence := range []string{fixBehaviorContractSentenceOne, fixBehaviorContractSentenceTwo} {
		if !strings.Contains(helpText, sentence) {
			t.Fatalf("fix help missing behavior contract sentence %q", sentence)
		}
	}

	repoRoot := mustRepoRoot(t)
	docsText, err := os.ReadFile(filepath.Join(repoRoot, "docs/commands/fix.md"))
	if err != nil {
		t.Fatalf("read fix docs: %v", err)
	}
	readmeText, err := os.ReadFile(filepath.Join(repoRoot, "README.md"))
	if err != nil {
		t.Fatalf("read README: %v", err)
	}
	for _, sentence := range []string{fixBehaviorContractSentenceOne, fixBehaviorContractSentenceTwo} {
		if !strings.Contains(string(docsText), sentence) {
			t.Fatalf("docs/commands/fix.md missing behavior contract sentence %q", sentence)
		}
		if !usesLandingReadmeVariant(string(readmeText)) && !strings.Contains(string(readmeText), sentence) {
			t.Fatalf("README.md missing behavior contract sentence %q", sentence)
		}
	}
}

func usesLandingReadmeVariant(readme string) bool {
	return strings.Contains(readme, "## Start Here")
}

func writeFixStateFixture(t *testing.T) string {
	t.Helper()

	tmp := t.TempDir()
	statePath := filepath.Join(tmp, "state.json")

	snapshot := state.Snapshot{
		Version: state.SnapshotVersion,
		Target:  source.Target{Mode: "repo", Value: "acme/backend"},
		Findings: []model.Finding{
			{FindingType: "policy_violation", RuleID: "WRKR-004", Severity: model.SeverityHigh, ToolType: "ci", Location: ".github/workflows/pr.yml", Repo: "backend", Org: "acme"},
			{FindingType: "skill_policy_conflict", Severity: model.SeverityHigh, ToolType: "skill", Location: ".agents/skills/release/SKILL.md", Repo: "backend", Org: "acme"},
			{FindingType: "mcp_server", Severity: model.SeverityMedium, ToolType: "mcp", Location: ".codex/config.toml", Repo: "backend", Org: "acme"},
		},
		RiskReport: &risk.Report{
			Ranked: []risk.ScoredFinding{
				{Score: 9.9, Finding: model.Finding{FindingType: "policy_violation", RuleID: "WRKR-004", Severity: model.SeverityHigh, ToolType: "ci", Location: ".github/workflows/pr.yml", Repo: "backend", Org: "acme"}},
				{Score: 9.4, Finding: model.Finding{FindingType: "skill_policy_conflict", Severity: model.SeverityHigh, ToolType: "skill", Location: ".agents/skills/release/SKILL.md", Repo: "backend", Org: "acme"}},
				{Score: 8.7, Finding: model.Finding{FindingType: "mcp_server", Severity: model.SeverityMedium, ToolType: "mcp", Location: ".codex/config.toml", Repo: "backend", Org: "acme"}},
				{Score: 8.0, Finding: model.Finding{FindingType: "unknown", Severity: model.SeverityLow, ToolType: "misc", Location: "README.md", Repo: "backend", Org: "acme"}},
			},
		},
	}

	if err := state.Save(statePath, snapshot); err != nil {
		t.Fatalf("save state fixture: %v", err)
	}
	return statePath
}

func writeApplyStateFixture(t *testing.T) string {
	t.Helper()

	tmp := t.TempDir()
	statePath := filepath.Join(tmp, "state.json")
	now := "2026-03-26T12:00:00Z"
	snapshot := state.Snapshot{
		Version: state.SnapshotVersion,
		Target:  source.Target{Mode: "repo", Value: "acme/backend"},
		Findings: []model.Finding{
			{FindingType: "tool_config", Severity: model.SeverityMedium, ToolType: "codex", Location: ".codex/config.toml", Repo: "backend", Org: "acme"},
		},
		RiskReport: &risk.Report{
			GeneratedAt: now,
			Ranked: []risk.ScoredFinding{
				{Score: 7.7, Finding: model.Finding{FindingType: "tool_config", Severity: model.SeverityMedium, ToolType: "codex", Location: ".codex/config.toml", Repo: "backend", Org: "acme"}},
			},
		},
		Inventory: &agginventory.Inventory{
			Org: "acme",
			Tools: []agginventory.Tool{
				{
					ToolID:          "codex-config",
					AgentID:         "wrkr:codex-config:acme",
					ToolType:        "codex",
					Org:             "acme",
					DataClass:       "source_code",
					EndpointClass:   "fs.read",
					AutonomyLevel:   "interactive",
					RiskScore:       7.7,
					ApprovalStatus:  "missing",
					ApprovalClass:   "under_review",
					LifecycleState:  "discovered",
					DiscoveryMethod: "static",
					Locations: []agginventory.ToolLocation{
						{Repo: "backend", Location: ".codex/config.toml"},
					},
				},
			},
		},
		Identities: []manifest.IdentityRecord{
			{
				AgentID:       "wrkr:codex-config:acme",
				ToolID:        "codex-config",
				ToolType:      "codex",
				Org:           "acme",
				Repo:          "backend",
				Location:      ".codex/config.toml",
				Status:        "discovered",
				ApprovalState: "missing",
				FirstSeen:     now,
				LastSeen:      now,
				Present:       true,
				DataClass:     "source_code",
				EndpointClass: "fs.read",
				AutonomyLevel: "interactive",
				RiskScore:     7.7,
			},
		},
	}

	if err := state.Save(statePath, snapshot); err != nil {
		t.Fatalf("save apply state fixture: %v", err)
	}
	return statePath
}

func mustRepoRoot(t *testing.T) string {
	t.Helper()

	wd, err := os.Getwd()
	if err != nil {
		t.Fatalf("getwd: %v", err)
	}
	current := wd
	for {
		if _, err := os.Stat(filepath.Join(current, "go.mod")); err == nil {
			return current
		}
		parent := filepath.Dir(current)
		if parent == current {
			t.Fatalf("could not find repo root from %s", wd)
		}
		current = parent
	}
}

type stubPRAPI struct {
	ensureFileCalls int
	paths           []string
	createCalls     int
}

func (s *stubPRAPI) EnsureHeadRef(context.Context, string, string, string, string) error {
	return nil
}

func (s *stubPRAPI) EnsureFileContent(_ context.Context, _ string, _ string, _ string, filePath string, _ string, _ []byte) (bool, error) {
	s.ensureFileCalls++
	s.paths = append(s.paths, filepath.ToSlash(strings.TrimSpace(filePath)))
	return true, nil
}

func (s *stubPRAPI) ListOpenByHead(context.Context, string, string, string, string) ([]githubpr.PullRequest, error) {
	return nil, nil
}

func (s *stubPRAPI) Create(context.Context, string, string, githubpr.CreateRequest) (githubpr.PullRequest, error) {
	s.createCalls++
	return githubpr.PullRequest{
		Number: 10 + s.createCalls,
		URL:    fmt.Sprintf("https://example.test/pr/%d", 10+s.createCalls),
		Title:  "wrkr remediation",
		Body:   "body",
		Head:   fmt.Sprintf("wrkr-bot/remediation/acme-backend/adhoc/%02d", s.createCalls),
		Base:   "main",
	}, nil
}

func (s *stubPRAPI) Update(context.Context, string, string, int, githubpr.UpdateRequest) (githubpr.PullRequest, error) {
	return githubpr.PullRequest{}, nil
}
