import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import type { PipelineStageItem } from "../api/client";
import { StagePanel } from "./StagePanel";

describe("StagePanel", () => {
	it("renders empty message when no items", () => {
		render(<StagePanel title="Current" items={[]} />);
		expect(screen.getByText("Empty.")).toBeInTheDocument();
	});

	it("renders story item without agent lozenge when agent is null", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "42_story_no_agent",
				name: "No Agent Story",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		expect(screen.getByText("No Agent Story")).toBeInTheDocument();
		// No agent lozenge
		expect(screen.queryByText(/coder-/)).not.toBeInTheDocument();
	});

	it("shows agent lozenge with agent name and model when agent is running", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "43_story_with_agent",
				name: "Active Story",
				error: null,
				merge_failure: null,
				agent: {
					agent_name: "coder-1",
					model: "sonnet",
					status: "running",
				},
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		expect(screen.getByText("Active Story")).toBeInTheDocument();
		expect(screen.getByText("coder-1 sonnet")).toBeInTheDocument();
	});

	it("shows agent lozenge with only agent name when model is null", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "44_story_no_model",
				name: "No Model Story",
				error: null,
				merge_failure: null,
				agent: {
					agent_name: "coder-2",
					model: null,
					status: "running",
				},
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		expect(screen.getByText("coder-2")).toBeInTheDocument();
	});

	it("shows agent lozenge for pending agent", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "45_story_pending",
				name: "Pending Story",
				error: null,
				merge_failure: null,
				agent: {
					agent_name: "coder-1",
					model: "haiku",
					status: "pending",
				},
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="QA" items={items} />);
		expect(screen.getByText("coder-1 haiku")).toBeInTheDocument();
	});

	it("shows story number extracted from story_id", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "59_story_current_work_panel",
				name: "Current Work Panel",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		expect(screen.getByText("#59")).toBeInTheDocument();
	});

	it("shows error message when item has an error", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "1_story_bad",
				name: "",
				error: "Missing front matter",
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Upcoming" items={items} />);
		expect(screen.getByText("Missing front matter")).toBeInTheDocument();
	});

	it("shows STORY badge for story items", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "10_story_some_feature",
				name: "Some Feature",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Upcoming" items={items} />);
		expect(
			screen.getByTestId("type-badge-10_story_some_feature"),
		).toHaveTextContent("STORY");
	});

	it("shows BUG badge for bug items", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "11_bug_broken_thing",
				name: "Broken Thing",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		expect(
			screen.getByTestId("type-badge-11_bug_broken_thing"),
		).toHaveTextContent("BUG");
	});

	it("shows SPIKE badge for spike items", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "12_spike_investigate_perf",
				name: "Investigate Perf",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="QA" items={items} />);
		expect(
			screen.getByTestId("type-badge-12_spike_investigate_perf"),
		).toHaveTextContent("SPIKE");
	});

	it("shows no badge for unrecognised type prefix", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "13_task_do_something",
				name: "Do Something",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Done" items={items} />);
		expect(
			screen.queryByTestId("type-badge-13_task_do_something"),
		).not.toBeInTheDocument();
	});

	it("card has uniform border on all sides for story items", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "20_story_uniform_border",
				name: "Uniform Border",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Upcoming" items={items} />);
		const card = screen.getByTestId("card-20_story_uniform_border");
		// No 3px colored left border - all sides match the uniform shorthand
		expect(card.style.borderLeft).not.toContain("3px");
		expect(card.style.borderLeft).toBe(card.style.borderTop);
		expect(card.style.borderLeft).toBe(card.style.borderRight);
		expect(card.style.borderLeft).toBe(card.style.borderBottom);
	});

	it("card has uniform border on all sides for bug items", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "21_bug_uniform_border",
				name: "Uniform Border Bug",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		const card = screen.getByTestId("card-21_bug_uniform_border");
		expect(card.style.borderLeft).not.toContain("3px");
		expect(card.style.borderLeft).toBe(card.style.borderTop);
	});

	it("card has uniform border on all sides for spike items", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "22_spike_uniform_border",
				name: "Uniform Border Spike",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="QA" items={items} />);
		const card = screen.getByTestId("card-22_spike_uniform_border");
		expect(card.style.borderLeft).not.toContain("3px");
		expect(card.style.borderLeft).toBe(card.style.borderTop);
	});

	it("card has uniform border on all sides for unrecognised type", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "23_task_uniform_border",
				name: "Uniform Border Task",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Done" items={items} />);
		const card = screen.getByTestId("card-23_task_uniform_border");
		expect(card.style.borderLeft).not.toContain("3px");
		expect(card.style.borderLeft).toBe(card.style.borderTop);
	});

	it("shows merge failure icon and reason when merge_failure is set", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "30_story_merge_failed",
				name: "Failed Merge Story",
				error: null,
				merge_failure: "Squash merge failed: conflicts in Cargo.lock",
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Merge" items={items} />);
		expect(
			screen.getByTestId("merge-failure-icon-30_story_merge_failed"),
		).toBeInTheDocument();
		expect(
			screen.getByTestId("merge-failure-reason-30_story_merge_failed"),
		).toHaveTextContent("Squash merge failed: conflicts in Cargo.lock");
	});

	it("does not show merge failure elements when merge_failure is null", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "31_story_no_failure",
				name: "Clean Story",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Merge" items={items} />);
		expect(
			screen.queryByTestId("merge-failure-icon-31_story_no_failure"),
		).not.toBeInTheDocument();
		expect(
			screen.queryByTestId("merge-failure-reason-31_story_no_failure"),
		).not.toBeInTheDocument();
	});

	it("shows merge-in-flight icon when story is in mergesInFlight set", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "40_story_merging",
				name: "Merging Story",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		const mergesInFlight = new Set(["40_story_merging"]);
		render(
			<StagePanel title="To Merge" items={items} mergesInFlight={mergesInFlight} />,
		);
		expect(
			screen.getByTestId("merge-in-flight-icon-40_story_merging"),
		).toBeInTheDocument();
	});

	it("does not show merge-in-flight icon when story is not in mergesInFlight set", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "41_story_not_merging",
				name: "Idle Story",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		const mergesInFlight = new Set(["99_story_other"]);
		render(
			<StagePanel
				title="To Merge"
				items={items}
				mergesInFlight={mergesInFlight}
			/>,
		);
		expect(
			screen.queryByTestId("merge-in-flight-icon-41_story_not_merging"),
		).not.toBeInTheDocument();
	});

	it("does not show merge-in-flight icon when mergesInFlight prop is absent", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "42_story_no_prop",
				name: "No Prop Story",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="To Merge" items={items} />);
		expect(
			screen.queryByTestId("merge-in-flight-icon-42_story_no_prop"),
		).not.toBeInTheDocument();
	});

	it("shows spinning RECOVERING badge for blocked item with running recovery agent", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "50_story_blocked_recovering",
				name: "Blocked Recovering Story",
				error: null,
				merge_failure: null,
				agent: { agent_name: "coder", model: "claude", status: "running" },
				review_hold: null,
				qa: null,
				depends_on: null,
				blocked: true,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		const badge = screen.getByTestId("blocked-badge-50_story_blocked_recovering");
		expect(badge).toBeInTheDocument();
		expect(badge).toHaveTextContent("RECOVERING");
	});

	it("shows QUEUED badge for blocked item with pending recovery agent", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "51_story_blocked_queued",
				name: "Blocked Queued Story",
				error: null,
				merge_failure: null,
				agent: { agent_name: "coder", model: "claude", status: "pending" },
				review_hold: null,
				qa: null,
				depends_on: null,
				blocked: true,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		const badge = screen.getByTestId("blocked-badge-51_story_blocked_queued");
		expect(badge).toBeInTheDocument();
		expect(badge).toHaveTextContent("QUEUED");
	});

	it("shows red BLOCKED badge for blocked item with no recovery agent", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "52_story_blocked_human",
				name: "Blocked Human Story",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
				blocked: true,
			},
		];
		render(<StagePanel title="Current" items={items} />);
		const badge = screen.getByTestId("blocked-badge-52_story_blocked_human");
		expect(badge).toBeInTheDocument();
		expect(badge).toHaveTextContent("BLOCKED");
	});

	it("shows spinning icon for merge_failure item with running mergemaster", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "53_story_merge_recovering",
				name: "Merge Recovering Story",
				error: null,
				merge_failure: "Squash merge failed: conflicts",
				agent: { agent_name: "mergemaster", model: "claude", status: "running" },
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Merge" items={items} />);
		const icon = screen.getByTestId("merge-failure-icon-53_story_merge_recovering");
		expect(icon).toBeInTheDocument();
		expect(icon).toHaveTextContent("⟳");
	});

	it("shows hourglass icon for merge_failure item with pending mergemaster", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "54_story_merge_queued",
				name: "Merge Queued Story",
				error: null,
				merge_failure: "Squash merge failed: conflicts",
				agent: { agent_name: "mergemaster", model: "claude", status: "pending" },
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Merge" items={items} />);
		const icon = screen.getByTestId("merge-failure-icon-54_story_merge_queued");
		expect(icon).toBeInTheDocument();
		expect(icon).toHaveTextContent("⏳");
	});

	it("renders gate output in a bounded box with expand and copy controls", () => {
		const items: PipelineStageItem[] = [
			{
				story_id: "60_story_gate_output",
				name: "Gate Output Story",
				error: null,
				merge_failure: "Quality gates failed: cargo test output here",
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Merge" items={items} />);
		expect(screen.getByTestId("gate-output-text")).toHaveTextContent(
			"Quality gates failed: cargo test output here",
		);
		expect(screen.getByTestId("gate-output-toggle")).toBeInTheDocument();
		expect(screen.getByTestId("gate-output-copy")).toBeInTheDocument();
	});

	it("expand toggle changes label from Expand to Collapse", async () => {
		const user = userEvent.setup();
		const items: PipelineStageItem[] = [
			{
				story_id: "61_story_expand",
				name: "Expand Story",
				error: null,
				merge_failure: "A".repeat(1000),
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		render(<StagePanel title="Merge" items={items} />);
		const toggle = screen.getByTestId("gate-output-toggle");
		expect(toggle).toHaveTextContent("Expand");
		await user.click(toggle);
		expect(toggle).toHaveTextContent("Collapse");
		await user.click(toggle);
		expect(toggle).toHaveTextContent("Expand");
	});
});

describe("StagePanel - defensive rendering", () => {
	it("renders without exception when a story is missing its name field", () => {
		const items = [
			{
				story_id: "60_story_no_name",
				name: undefined as unknown as string,
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		expect(() => render(<StagePanel title="Current" items={items} />)).not.toThrow();
		expect(screen.getByTestId("card-60_story_no_name")).toBeInTheDocument();
	});

	it("renders without exception when a story is missing its story_id field", () => {
		const items = [
			{
				story_id: undefined as unknown as string,
				name: "Orphaned Story",
				error: null,
				merge_failure: null,
				agent: null,
				review_hold: null,
				qa: null,
				depends_on: null,
			},
		];
		expect(() => render(<StagePanel title="Current" items={items} />)).not.toThrow();
		expect(screen.getByText("Orphaned Story")).toBeInTheDocument();
	});
});
