package orch

import (
	"os"
	"path/filepath"
	"testing"
)

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

	dbPath := filepath.Join(t.TempDir(), "coord.db")

	initOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"run", "init",
		"--run", "run_blog_init_001",
		"--goal", "Build blog MVP",
		"--summary", "Public blog plus admin CRUD",
	)

	var initResp map[string]any
	mustDecodeJSON(t, initOut, &initResp)
	if got := nestedString(t, initResp, "data", "run", "run_id"); got != "run_blog_init_001" {
		t.Fatalf("expected run id run_blog_init_001, got %q", got)
	}
	if got := nestedString(t, initResp, "data", "run", "goal"); got != "Build blog MVP" {
		t.Fatalf("expected goal Build blog MVP, got %q", got)
	}
	if got := nestedString(t, initResp, "data", "run", "summary"); got != "Public blog plus admin CRUD" {
		t.Fatalf("expected summary to round-trip, got %q", got)
	}
	if got := nestedString(t, initResp, "data", "run", "status"); got != "active" {
		t.Fatalf("expected new run status active, got %q", got)
	}
	assertNonEmptyNestedString(t, initResp, "data", "run", "created_at")
	assertNonEmptyNestedString(t, initResp, "data", "run", "updated_at")

	showOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"run", "show",
		"--run", "run_blog_init_001",
	)

	var showResp map[string]any
	mustDecodeJSON(t, showOut, &showResp)
	if got := nestedString(t, showResp, "data", "run", "run_id"); got != "run_blog_init_001" {
		t.Fatalf("expected persisted run id run_blog_init_001, got %q", got)
	}
	if got := nestedString(t, showResp, "data", "run", "status"); got != "active" {
		t.Fatalf("expected persisted run status active, got %q", got)
	}
}

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

	dbPath := filepath.Join(t.TempDir(), "coord.db")

	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"run", "init",
		"--run", "run_blog_dispatch_001",
		"--goal", "Build blog MVP",
		"--summary", "Public blog plus admin CRUD",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"task", "add",
		"--run", "run_blog_dispatch_001",
		"--task", "T1",
		"--title", "Implement retry policy",
		"--summary", "Add retry policy to HTTP client",
		"--default-to", "worker-a",
	)

	dispatchOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"dispatch",
		"--run", "run_blog_dispatch_001",
		"--task", "T1",
		"--body", "Implement retry handling for the HTTP client.",
	)

	var dispatchResp map[string]any
	mustDecodeJSON(t, dispatchOut, &dispatchResp)
	if got := nestedString(t, dispatchResp, "data", "task", "status"); got != "dispatched" {
		t.Fatalf("expected dispatched task status, got %q", got)
	}
	if got := nestedValue(t, dispatchResp, "data", "attempt", "attempt_no").(float64); got != 1 {
		t.Fatalf("expected attempt_no 1, got %#v", got)
	}
	threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
	if threadID == "" {
		t.Fatal("expected non-empty attempt thread_id")
	}
	if got := nestedString(t, dispatchResp, "data", "attempt", "assigned_to"); got != "worker-a" {
		t.Fatalf("expected assigned_to worker-a, got %q", got)
	}
	if got := nestedString(t, dispatchResp, "data", "thread", "thread_id"); got != threadID {
		t.Fatalf("expected thread.thread_id %q, got %q", threadID, got)
	}
	if got := nestedString(t, dispatchResp, "data", "message", "kind"); got != "task" {
		t.Fatalf("expected first dispatch message kind task, got %q", got)
	}
}

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

	dbPath := filepath.Join(t.TempDir(), "coord.db")

	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"run", "init",
		"--run", "run_blog_blocked_001",
		"--goal", "Build dependency-aware workflow",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"task", "add",
		"--run", "run_blog_blocked_001",
		"--task", "T1",
		"--title", "Build backend",
		"--summary", "Implement backend APIs",
		"--default-to", "worker-a",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"task", "add",
		"--run", "run_blog_blocked_001",
		"--task", "T2",
		"--title", "Build frontend",
		"--summary", "Implement frontend flows",
		"--default-to", "worker-b",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"dep", "add",
		"--run", "run_blog_blocked_001",
		"--task", "T2",
		"--depends-on", "T1",
	)

	firstDispatch := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"dispatch",
		"--run", "run_blog_blocked_001",
		"--task", "T1",
	)

	var firstDispatchResp map[string]any
	mustDecodeJSON(t, firstDispatch, &firstDispatchResp)
	threadBackend := nestedString(t, firstDispatchResp, "data", "attempt", "thread_id")

	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"claim",
		"--agent", "worker-a",
		"--thread", threadBackend,
	)
	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"done",
		"--agent", "worker-a",
		"--thread", threadBackend,
		"--summary", "Backend complete",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"reconcile",
		"--run", "run_blog_blocked_001",
	)

	secondDispatch := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"dispatch",
		"--run", "run_blog_blocked_001",
		"--task", "T2",
	)

	var secondDispatchResp map[string]any
	mustDecodeJSON(t, secondDispatch, &secondDispatchResp)
	threadFrontend := nestedString(t, secondDispatchResp, "data", "attempt", "thread_id")

	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"claim",
		"--agent", "worker-b",
		"--thread", threadFrontend,
	)
	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"update",
		"--agent", "worker-b",
		"--thread", threadFrontend,
		"--status", "blocked",
		"--summary", "Need logging decision",
		"--payload-json", `{"question":"stdout or stderr?"}`,
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"reconcile",
		"--run", "run_blog_blocked_001",
	)

	blockedOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"blocked",
		"--run", "run_blog_blocked_001",
	)

	var blockedResp map[string]any
	mustDecodeJSON(t, blockedOut, &blockedResp)
	blockedTasks := nestedArray(t, blockedResp, "data", "blocked")
	if len(blockedTasks) != 1 {
		t.Fatalf("expected one blocked task, got %#v", blockedTasks)
	}
	blockedTask, ok := blockedTasks[0].(map[string]any)
	if !ok {
		t.Fatalf("expected blocked task object, got %#v", blockedTasks[0])
	}
	if got := nestedString(t, blockedTask, "task", "task_id"); got != "T2" {
		t.Fatalf("expected blocked task T2, got %q", got)
	}
	if got := nestedString(t, blockedTask, "question", "kind"); got != "question" {
		t.Fatalf("expected question.kind=question, got %q", got)
	}
	if got := nestedString(t, blockedTask, "question", "summary"); got != "Need logging decision" {
		t.Fatalf("expected question summary to match latest blocked message, got %q", got)
	}
	questionPayload, ok := nestedValue(t, blockedTask, "question", "payload_json").(map[string]any)
	if !ok {
		t.Fatalf("expected question payload_json object, got %#v", nestedValue(t, blockedTask, "question", "payload_json"))
	}
	if got, _ := questionPayload["question"].(string); got != "stdout or stderr?" {
		t.Fatalf("expected latest question payload, got %#v", questionPayload["question"])
	}
}

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

	dbPath := filepath.Join(t.TempDir(), "coord.db")

	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"run", "init",
		"--run", "run_blog_status_001",
		"--goal", "Build blog MVP",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"task", "add",
		"--run", "run_blog_status_001",
		"--task", "T1",
		"--title", "Implement retry policy",
		"--default-to", "worker-a",
	)

	dispatchOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"dispatch",
		"--run", "run_blog_status_001",
		"--task", "T1",
		"--body", "Implement retry handling for the HTTP client.",
	)

	var dispatchResp map[string]any
	mustDecodeJSON(t, dispatchOut, &dispatchResp)
	threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")

	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"claim",
		"--agent", "worker-a",
		"--thread", threadID,
	)
	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"done",
		"--agent", "worker-a",
		"--thread", threadID,
		"--summary", "Retry policy implemented",
		"--body", "The HTTP client now retries transient failures.",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"reconcile",
		"--run", "run_blog_status_001",
	)

	statusOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"status",
		"--run", "run_blog_status_001",
	)

	var statusResp map[string]any
	mustDecodeJSON(t, statusOut, &statusResp)
	if got := nestedString(t, statusResp, "data", "run", "run_id"); got != "run_blog_status_001" {
		t.Fatalf("expected run_id run_blog_status_001, got %q", got)
	}
	if got := nestedString(t, statusResp, "data", "run", "status"); got != "done" {
		t.Fatalf("expected run status done, got %q", got)
	}
	taskCounts, ok := nestedValue(t, statusResp, "data", "task_counts").(map[string]any)
	if !ok {
		t.Fatalf("expected task_counts object, got %#v", nestedValue(t, statusResp, "data", "task_counts"))
	}
	if got, _ := taskCounts["done"].(float64); got != 1 {
		t.Fatalf("expected done task count 1, got %#v", taskCounts["done"])
	}
	tasks := nestedArray(t, statusResp, "data", "tasks")
	if len(tasks) != 1 {
		t.Fatalf("expected one task in status response, got %#v", tasks)
	}
	task, ok := tasks[0].(map[string]any)
	if !ok {
		t.Fatalf("expected task object, got %#v", tasks[0])
	}
	if got, _ := task["task_id"].(string); got != "T1" {
		t.Fatalf("expected task_id T1, got %#v", task["task_id"])
	}
	if got, _ := task["status"].(string); got != "done" {
		t.Fatalf("expected task status done, got %#v", task["status"])
	}
}

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

	dbPath := filepath.Join(t.TempDir(), "coord.db")

	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"run", "init",
		"--run", "run_blog_reconcile_001",
		"--goal", "Build blog MVP",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"task", "add",
		"--run", "run_blog_reconcile_001",
		"--task", "T1",
		"--title", "Implement retry policy",
		"--default-to", "worker-a",
	)

	dispatchOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"dispatch",
		"--run", "run_blog_reconcile_001",
		"--task", "T1",
		"--body", "Implement retry handling for the HTTP client.",
	)

	var dispatchResp map[string]any
	mustDecodeJSON(t, dispatchOut, &dispatchResp)
	threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")

	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"claim",
		"--agent", "worker-a",
		"--thread", threadID,
	)
	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"fail",
		"--agent", "worker-a",
		"--thread", threadID,
		"--summary", "Retry policy failed",
		"--body", "The HTTP client kept failing integration tests.",
	)

	reconcileOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"reconcile",
		"--run", "run_blog_reconcile_001",
	)

	var reconcileResp map[string]any
	mustDecodeJSON(t, reconcileOut, &reconcileResp)
	updatedTasks := nestedArray(t, reconcileResp, "data", "updated_tasks")
	if len(updatedTasks) != 1 {
		t.Fatalf("expected one updated task after failed reconcile, got %#v", updatedTasks)
	}
	task, ok := updatedTasks[0].(map[string]any)
	if !ok {
		t.Fatalf("expected updated task object, got %#v", updatedTasks[0])
	}
	if got, _ := task["task_id"].(string); got != "T1" {
		t.Fatalf("expected updated task T1, got %#v", task["task_id"])
	}
	if got, _ := task["status"].(string); got != "failed" {
		t.Fatalf("expected reconciled task status failed, got %#v", task["status"])
	}
	taskCounts, ok := nestedValue(t, reconcileResp, "data", "task_counts").(map[string]any)
	if !ok {
		t.Fatalf("expected task_counts object, got %#v", nestedValue(t, reconcileResp, "data", "task_counts"))
	}
	if got, _ := taskCounts["failed"].(float64); got != 1 {
		t.Fatalf("expected failed task count 1 after reconcile, got %#v", taskCounts["failed"])
	}

	statusOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"status",
		"--run", "run_blog_reconcile_001",
	)

	var statusResp map[string]any
	mustDecodeJSON(t, statusOut, &statusResp)
	if got := nestedString(t, statusResp, "data", "run", "status"); got != "failed" {
		t.Fatalf("expected run status failed after failed reconcile, got %q", got)
	}
}

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

	dbPath := filepath.Join(t.TempDir(), "coord.db")
	repoPath := initGitRepo(t)

	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"run", "init",
		"--run", "run_blog_workflow_worktree_001",
		"--goal", "Validate strict worktree dispatch",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"task", "add",
		"--run", "run_blog_workflow_worktree_001",
		"--task", "T1",
		"--title", "Implement backend",
		"--default-to", "worker-a",
	)

	dispatchOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"dispatch",
		"--run", "run_blog_workflow_worktree_001",
		"--task", "T1",
		"--repo-path", repoPath,
		"--workspace-root", ".orch/worktrees",
		"--strict-worktree",
		"--body", "Implement inside isolated worktree.",
	)

	var dispatchResp map[string]any
	mustDecodeJSON(t, dispatchOut, &dispatchResp)
	threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
	worktreePath := nestedString(t, dispatchResp, "data", "attempt", "worktree_path")
	if worktreePath == "" {
		t.Fatal("expected non-empty worktree_path for strict worktree workflow")
	}
	if got := nestedString(t, dispatchResp, "data", "attempt", "workspace_status"); got != "created" {
		t.Fatalf("expected workspace_status created, got %q", got)
	}

	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"claim",
		"--agent", "worker-a",
		"--thread", threadID,
	)
	runInboxCommand(
		t,
		"--db", dbPath,
		"--json",
		"done",
		"--agent", "worker-a",
		"--thread", threadID,
		"--summary", "Backend complete",
	)
	runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"reconcile",
		"--run", "run_blog_workflow_worktree_001",
	)

	cleanupOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"cleanup",
		"--run", "run_blog_workflow_worktree_001",
		"--task", "T1",
		"--attempt", "1",
	)

	var cleanupResp map[string]any
	mustDecodeJSON(t, cleanupOut, &cleanupResp)
	cleaned := nestedArray(t, cleanupResp, "data", "cleaned")
	if len(cleaned) != 1 {
		t.Fatalf("expected one cleaned attempt, got %#v", cleaned)
	}
	cleanedAttempt, ok := cleaned[0].(map[string]any)
	if !ok {
		t.Fatalf("expected cleaned attempt object, got %#v", cleaned[0])
	}
	if got, _ := cleanedAttempt["workspace_status"].(string); got != "cleaned" {
		t.Fatalf("expected cleaned workspace_status, got %#v", cleanedAttempt["workspace_status"])
	}
	if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
		t.Fatalf("expected cleaned worktree path to be removed, err=%v", err)
	}
}

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

	dbPath := filepath.Join(t.TempDir(), "coord.db")
	runID := "council_blog_workflow_001"

	startOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"council", "start",
		"--run", runID,
		"--target", "Review the current blog architecture.",
	)

	var startResp map[string]any
	mustDecodeJSON(t, startOut, &startResp)
	reviewers := nestedArray(t, startResp, "data", "reviewers")
	if len(reviewers) != 3 {
		t.Fatalf("expected three reviewers from council start, got %#v", reviewers)
	}

	completeCouncilWorkflowReviewersForRemainingTests(t, dbPath, runID)

	waitOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"council", "wait",
		"--run", runID,
		"--timeout-seconds", "2",
	)

	var waitResp map[string]any
	mustDecodeJSON(t, waitOut, &waitResp)
	if woke, _ := nestedValue(t, waitResp, "data", "woke").(bool); !woke {
		t.Fatalf("expected council wait to wake, got %#v", waitResp)
	}
	if allComplete, _ := nestedValue(t, waitResp, "data", "all_complete").(bool); !allComplete {
		t.Fatalf("expected all reviewers complete, got %#v", waitResp)
	}

	tallyOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"council", "tally",
		"--run", runID,
		"--similarity", "normal",
	)

	var tallyResp map[string]any
	mustDecodeJSON(t, tallyOut, &tallyResp)
	if got := nestedString(t, tallyResp, "data", "similarity"); got != "normal" {
		t.Fatalf("expected normal tally similarity, got %q", got)
	}
	tallyCounts, ok := nestedValue(t, tallyResp, "data", "counts").(map[string]any)
	if !ok {
		t.Fatalf("expected tally counts object, got %#v", nestedValue(t, tallyResp, "data", "counts"))
	}
	if got, _ := tallyCounts["consensus"].(float64); got != 1 {
		t.Fatalf("expected one consensus group, got %#v", tallyCounts["consensus"])
	}
	if got, _ := tallyCounts["majority"].(float64); got != 1 {
		t.Fatalf("expected one majority group, got %#v", tallyCounts["majority"])
	}
	if got, _ := tallyCounts["minority"].(float64); got != 1 {
		t.Fatalf("expected one minority group, got %#v", tallyCounts["minority"])
	}

	reportOut := runOrchCommand(
		t,
		"--db", dbPath,
		"--json",
		"council", "report",
		"--run", runID,
	)

	var reportResp map[string]any
	mustDecodeJSON(t, reportOut, &reportResp)
	show := nestedArray(t, reportResp, "data", "show")
	if len(show) != 2 || show[0] != "consensus" || show[1] != "majority" {
		t.Fatalf("expected default report show [consensus majority], got %#v", show)
	}
	grouped := nestedArray(t, reportResp, "data", "grouped_recommendations")
	if len(grouped) != 2 {
		t.Fatalf("expected default report to include consensus and majority groups, got %#v", grouped)
	}
	artifacts := nestedArray(t, reportResp, "data", "report_artifacts")
	if len(artifacts) != 1 {
		t.Fatalf("expected one report artifact, got %#v", artifacts)
	}
	artifact, ok := artifacts[0].(map[string]any)
	if !ok {
		t.Fatalf("expected report artifact object, got %#v", artifacts[0])
	}
	reportPath, _ := artifact["path"].(string)
	if reportPath == "" {
		t.Fatalf("expected report artifact path, got %#v", artifact["path"])
	}
	if _, err := os.Stat(reportPath); err != nil {
		t.Fatalf("expected report artifact to exist at %q: %v", reportPath, err)
	}
}

func assertNonEmptyNestedString(t *testing.T, value map[string]any, keys ...string) {
	t.Helper()

	if got := nestedString(t, value, keys...); got == "" {
		t.Fatalf("expected non-empty string at %v", keys)
	}
}

func completeCouncilWorkflowReviewersForRemainingTests(t *testing.T, dbPath, runID string) {
	t.Helper()

	completeCouncilReviewer(
		t,
		dbPath,
		runID,
		"architecture-reviewer",
		`{"reviewer_role":"architecture-reviewer","findings":[{"title":"Split contracts","summary":"Transport contracts are mixed into UI code.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This lowers coupling.","confidence":"high","tags":["architecture"],"target_refs":{"repo_path":"."}},{"title":"Share helpers","summary":"Council report rendering paths are repeated.","proposal":"Introduce shared council coordinator helpers for report rendering.","rationale":"This keeps report assembly consistent.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`,
	)
	completeCouncilReviewer(
		t,
		dbPath,
		runID,
		"implementation-reviewer",
		`{"reviewer_role":"implementation-reviewer","findings":[{"title":"Extract contracts","summary":"Shared transport shapes are duplicated.","proposal":"Move API contract definitions into dedicated module","rationale":"This reduces duplication.","confidence":"high","tags":["maintainability"],"target_refs":{"repo_path":"."}},{"title":"Reuse report helpers","summary":"Formatting logic should stay shared.","proposal":"Introduce shared council coordinator helpers for report rendering","rationale":"This avoids formatter drift.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`,
	)
	completeCouncilReviewer(
		t,
		dbPath,
		runID,
		"risk-reviewer",
		`{"reviewer_role":"risk-reviewer","findings":[{"title":"Lock contracts","summary":"Contract drift becomes risky over time.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This reduces integration regressions.","confidence":"high","tags":["risk"],"target_refs":{"repo_path":"."}},{"title":"Cover JSON output","summary":"The council report response should stay stable.","proposal":"Add regression tests for council report JSON output.","rationale":"This catches contract regressions earlier.","confidence":"high","tags":["testing"],"target_refs":{"repo_path":"."}}]}`,
	)
}
