import { act, render, screen } from "@testing-library/react";
import * as React from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { PipelineState } from "../api/client";
import { LozengeFlyProvider, useLozengeFly } from "./LozengeFlyContext";
import { StagePanel } from "./StagePanel";

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
	return {
		upcoming: [],
		current: [],
		qa: [],
		merge: [],
		...overrides,
	};
}

/** A minimal roster element fixture that registers itself with the context. */
function RosterFixture({ agentName }: { agentName: string }) {
	const { registerRosterEl } = useLozengeFly();
	const ref = React.useRef<HTMLSpanElement>(null);
	React.useEffect(() => {
		const el = ref.current;
		if (el) registerRosterEl(agentName, el);
		return () => registerRosterEl(agentName, null);
	}, [agentName, registerRosterEl]);
	return (
		<span
			ref={ref}
			data-testid={`roster-${agentName}`}
			style={{ position: "fixed", top: 10, left: 20, width: 80, height: 20 }}
		/>
	);
}

function Wrapper({
	pipeline,
	children,
}: {
	pipeline: PipelineState;
	children: React.ReactNode;
}) {
	return (
		<LozengeFlyProvider pipeline={pipeline}>{children}</LozengeFlyProvider>
	);
}

// ─── Agent lozenge fixed intrinsic width ──────────────────────────────────────

describe("AgentLozenge fixed intrinsic width", () => {
	it("has align-self: flex-start so it never stretches inside a flex column", () => {
		const items = [
			{
				story_id: "74_width_test",
				name: "Width Test",
				error: null,
				agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
			},
		];
		const pipeline = makePipeline({ current: items });
		const { container } = render(
			<Wrapper pipeline={pipeline}>
				<StagePanel title="Current" items={items} />
			</Wrapper>,
		);

		const lozenge = container.querySelector(
			'[data-testid="slot-lozenge-74_width_test"]',
		) as HTMLElement;
		expect(lozenge).toBeInTheDocument();
		expect(lozenge.style.alignSelf).toBe("flex-start");
	});
});

// ─── Fly-in: slot lozenge visibility ─────────────────────────────────────────

describe("LozengeFlyProvider fly-in visibility", () => {
	beforeEach(() => {
		Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
			left: 100,
			top: 50,
			right: 180,
			bottom: 70,
			width: 80,
			height: 20,
			x: 100,
			y: 50,
			toJSON: () => ({}),
		});
		vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
			cb(0);
			return 0;
		});
	});

	afterEach(() => {
		vi.restoreAllMocks();
	});

	it("slot lozenge starts hidden when a matching roster element exists", async () => {
		const noPipeline = makePipeline();
		const withAgent = makePipeline({
			current: [
				{
					story_id: "74_hidden_test",
					name: "Hidden Test",
					error: null,
					agent: { agent_name: "coder-1", model: null, status: "running" },
				},
			],
		});

		const { rerender } = render(
			<Wrapper pipeline={noPipeline}>
				<RosterFixture agentName="coder-1" />
				<StagePanel title="Current" items={[]} />
			</Wrapper>,
		);

		// Rerender with the agent assigned
		await act(async () => {
			rerender(
				<Wrapper pipeline={withAgent}>
					<RosterFixture agentName="coder-1" />
					<StagePanel title="Current" items={withAgent.current} />
				</Wrapper>,
			);
		});

		const lozenge = screen.getByTestId("slot-lozenge-74_hidden_test");
		// Hidden while fly-in is in progress
		expect(lozenge.style.opacity).toBe("0");
	});

	it("slot lozenge is visible when no roster element is registered", async () => {
		const noPipeline = makePipeline();
		const withAgent = makePipeline({
			current: [
				{
					story_id: "74_no_roster",
					name: "No Roster",
					error: null,
					agent: {
						agent_name: "unknown-agent",
						model: null,
						status: "running",
					},
				},
			],
		});

		const { rerender } = render(
			<Wrapper pipeline={noPipeline}>
				{/* No RosterFixture for "unknown-agent" */}
				<StagePanel title="Current" items={[]} />
			</Wrapper>,
		);

		await act(async () => {
			rerender(
				<Wrapper pipeline={withAgent}>
					<StagePanel title="Current" items={withAgent.current} />
				</Wrapper>,
			);
		});

		const lozenge = screen.getByTestId("slot-lozenge-74_no_roster");
		// Immediately visible because no fly-in animation is possible
		expect(lozenge.style.opacity).toBe("1");
	});
});

// ─── Fly-in: flying clone in document.body portal ────────────────────────────

describe("LozengeFlyProvider fly-in clone", () => {
	beforeEach(() => {
		vi.useFakeTimers();
		Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
			left: 100,
			top: 50,
			right: 180,
			bottom: 70,
			width: 80,
			height: 20,
			x: 100,
			y: 50,
			toJSON: () => ({}),
		});
		vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
			cb(0);
			return 0;
		});
	});

	afterEach(() => {
		vi.useRealTimers();
		vi.restoreAllMocks();
	});

	it("renders a fixed-position clone in document.body when fly-in triggers", async () => {
		const noPipeline = makePipeline();
		const withAgent = makePipeline({
			current: [
				{
					story_id: "74_portal_test",
					name: "Portal Test",
					error: null,
					agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
				},
			],
		});

		const { rerender } = render(
			<Wrapper pipeline={noPipeline}>
				<RosterFixture agentName="coder-1" />
				<StagePanel title="Current" items={[]} />
			</Wrapper>,
		);

		await act(async () => {
			rerender(
				<Wrapper pipeline={withAgent}>
					<RosterFixture agentName="coder-1" />
					<StagePanel title="Current" items={withAgent.current} />
				</Wrapper>,
			);
			vi.runAllTimers();
		});

		// Clone is in document.body (portal), not inside the component container
		const clone = document.body.querySelector(
			'[data-testid^="flying-lozenge-fly-in"]',
		) as HTMLElement | null;
		expect(clone).not.toBeNull();
		expect(clone?.style.position).toBe("fixed");
		expect(Number(clone?.style.zIndex)).toBeGreaterThanOrEqual(9999);
		expect(clone?.style.pointerEvents).toBe("none");
	});

	it("clone is removed from document.body after 500 ms", async () => {
		const noPipeline = makePipeline();
		const withAgent = makePipeline({
			current: [
				{
					story_id: "74_clone_remove",
					name: "Clone Remove",
					error: null,
					agent: { agent_name: "coder-1", model: null, status: "running" },
				},
			],
		});

		const { rerender } = render(
			<Wrapper pipeline={noPipeline}>
				<RosterFixture agentName="coder-1" />
				<StagePanel title="Current" items={[]} />
			</Wrapper>,
		);

		await act(async () => {
			rerender(
				<Wrapper pipeline={withAgent}>
					<RosterFixture agentName="coder-1" />
					<StagePanel title="Current" items={withAgent.current} />
				</Wrapper>,
			);
		});

		// Clone should exist before timeout
		const cloneBefore = document.body.querySelector(
			'[data-testid^="flying-lozenge-fly-in"]',
		);
		expect(cloneBefore).not.toBeNull();

		// Advance past the 500ms cleanup timeout
		await act(async () => {
			vi.advanceTimersByTime(600);
		});

		const cloneAfter = document.body.querySelector(
			'[data-testid^="flying-lozenge-fly-in"]',
		);
		expect(cloneAfter).toBeNull();
	});

	it("slot lozenge becomes visible (opacity 1) after 500 ms timeout", async () => {
		const noPipeline = makePipeline();
		const withAgent = makePipeline({
			current: [
				{
					story_id: "74_reveal_test",
					name: "Reveal Test",
					error: null,
					agent: { agent_name: "coder-1", model: null, status: "running" },
				},
			],
		});

		const { rerender } = render(
			<Wrapper pipeline={noPipeline}>
				<RosterFixture agentName="coder-1" />
				<StagePanel title="Current" items={[]} />
			</Wrapper>,
		);

		await act(async () => {
			rerender(
				<Wrapper pipeline={withAgent}>
					<RosterFixture agentName="coder-1" />
					<StagePanel title="Current" items={withAgent.current} />
				</Wrapper>,
			);
		});

		// Initially hidden
		const lozenge = screen.getByTestId("slot-lozenge-74_reveal_test");
		expect(lozenge.style.opacity).toBe("0");

		// After 500ms the slot becomes visible
		await act(async () => {
			vi.advanceTimersByTime(600);
		});

		expect(lozenge.style.opacity).toBe("1");
	});
});

// ─── Fly-out animation ────────────────────────────────────────────────────────

describe("LozengeFlyProvider fly-out", () => {
	beforeEach(() => {
		vi.useFakeTimers();
		Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
			left: 100,
			top: 50,
			right: 180,
			bottom: 70,
			width: 80,
			height: 20,
			x: 100,
			y: 50,
			toJSON: () => ({}),
		});
		vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
			cb(0);
			return 0;
		});
	});

	afterEach(() => {
		vi.useRealTimers();
		vi.restoreAllMocks();
	});

	it("creates a fly-out clone in document.body when agent is removed", async () => {
		const withAgent = makePipeline({
			current: [
				{
					story_id: "74_fly_out_test",
					name: "Fly Out Test",
					error: null,
					agent: { agent_name: "coder-1", model: "haiku", status: "completed" },
				},
			],
		});

		const { rerender } = render(
			<Wrapper pipeline={withAgent}>
				<RosterFixture agentName="coder-1" />
				<StagePanel title="Current" items={withAgent.current} />
			</Wrapper>,
		);

		// Advance past initial fly-in animation to get a clean state
		await act(async () => {
			vi.advanceTimersByTime(600);
		});

		// Remove the agent from the pipeline
		const noAgent = makePipeline({
			current: [
				{
					story_id: "74_fly_out_test",
					name: "Fly Out Test",
					error: null,
					agent: null,
				},
			],
		});

		await act(async () => {
			rerender(
				<Wrapper pipeline={noAgent}>
					<RosterFixture agentName="coder-1" />
					<StagePanel title="Current" items={noAgent.current} />
				</Wrapper>,
			);
		});

		// A fly-out clone should now be in document.body
		const clone = document.body.querySelector(
			'[data-testid^="flying-lozenge-fly-out"]',
		);
		expect(clone).not.toBeNull();
	});
});

// ─── Idle vs active visual distinction ────────────────────────────────────────

describe("AgentLozenge idle vs active appearance", () => {
	it("running agent lozenge uses the green active color", () => {
		const items = [
			{
				story_id: "74_running_color",
				name: "Running",
				error: null,
				agent: { agent_name: "coder-1", model: null, status: "running" },
			},
		];
		const { container } = render(
			<Wrapper pipeline={makePipeline({ current: items })}>
				<StagePanel title="Current" items={items} />
			</Wrapper>,
		);

		const lozenge = container.querySelector(
			'[data-testid="slot-lozenge-74_running_color"]',
		) as HTMLElement;
		expect(lozenge).toBeInTheDocument();
		// Green: rgb(63, 185, 80) = #3fb950
		expect(lozenge.style.color).toBe("rgb(63, 185, 80)");
	});

	it("pending agent lozenge uses the yellow pending color", () => {
		const items = [
			{
				story_id: "74_pending_color",
				name: "Pending",
				error: null,
				agent: { agent_name: "coder-1", model: null, status: "pending" },
			},
		];
		const { container } = render(
			<Wrapper pipeline={makePipeline({ current: items })}>
				<StagePanel title="Current" items={items} />
			</Wrapper>,
		);

		const lozenge = container.querySelector(
			'[data-testid="slot-lozenge-74_pending_color"]',
		) as HTMLElement;
		expect(lozenge).toBeInTheDocument();
		// Yellow: rgb(227, 179, 65) = #e3b341
		expect(lozenge.style.color).toBe("rgb(227, 179, 65)");
	});

	it("running lozenge has a pulsing dot child element", () => {
		const items = [
			{
				story_id: "74_pulse_dot",
				name: "Pulse",
				error: null,
				agent: { agent_name: "coder-1", model: null, status: "running" },
			},
		];
		const { container } = render(
			<Wrapper pipeline={makePipeline({ current: items })}>
				<StagePanel title="Current" items={items} />
			</Wrapper>,
		);

		const lozenge = container.querySelector(
			'[data-testid="slot-lozenge-74_pulse_dot"]',
		) as HTMLElement;
		// The pulse dot is a child span with animation: pulse
		const dot = lozenge.querySelector("span");
		expect(dot).not.toBeNull();
		expect(dot?.style.animation).toContain("pulse");
	});
});
