import { act, render, screen } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { AgentConfigInfo, AgentEvent, AgentInfo } from "../api/agents";
import { agentsApi, subscribeAgentStream } from "../api/agents";

vi.mock("../api/agents", () => {
	const agentsApi = {
		listAgents: vi.fn(),
		getAgentConfig: vi.fn(),
		startAgent: vi.fn(),
		stopAgent: vi.fn(),
		reloadConfig: vi.fn(),
	};
	return { agentsApi, subscribeAgentStream: vi.fn(() => () => {}) };
});

// Dynamic import so the mock is in place before the module loads
const { AgentPanel } = await import("./AgentPanel");

const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream);

const mockedAgents = {
	listAgents: vi.mocked(agentsApi.listAgents),
	getAgentConfig: vi.mocked(agentsApi.getAgentConfig),
	startAgent: vi.mocked(agentsApi.startAgent),
};

const ROSTER: AgentConfigInfo[] = [
	{
		name: "coder-1",
		role: "Full-stack engineer",
		model: "sonnet",
		allowed_tools: null,
		max_turns: 50,
		max_budget_usd: 5.0,
	},
];

describe("AgentPanel active work list removed", () => {
	beforeAll(() => {
		Element.prototype.scrollIntoView = vi.fn();
	});

	beforeEach(() => {
		mockedAgents.getAgentConfig.mockResolvedValue(ROSTER);
		mockedAgents.listAgents.mockResolvedValue([]);
	});

	it("does not render active agent entries even when agents are running", async () => {
		const agentList: AgentInfo[] = [
			{
				story_id: "83_active",
				agent_name: "coder-1",
				status: "running",
				session_id: null,
				worktree_path: "/tmp/wt",
				base_branch: "master",
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		const { container } = render(<AgentPanel />);

		// Roster badge should still be visible
		await screen.findByTestId("roster-badge-coder-1");

		// No agent entry divs should exist
		expect(
			container.querySelector('[data-testid^="agent-entry-"]'),
		).not.toBeInTheDocument();
	});
});

describe("Running count visibility in header", () => {
	beforeAll(() => {
		Element.prototype.scrollIntoView = vi.fn();
	});

	beforeEach(() => {
		mockedAgents.getAgentConfig.mockResolvedValue(ROSTER);
		mockedAgents.listAgents.mockResolvedValue([]);
	});

	// AC1: When no agents are running, "0 running" is NOT visible
	it("does not show running count when no agents are running", async () => {
		render(<AgentPanel />);

		// Wait for roster to load
		await screen.findByTestId("roster-badge-coder-1");

		expect(screen.queryByText(/0 running/)).not.toBeInTheDocument();
	});

	// AC2: When agents are running, "N running" IS visible
	it("shows running count when agents are running", async () => {
		const agentList: AgentInfo[] = [
			{
				story_id: "99_active",
				agent_name: "coder-1",
				status: "running",
				session_id: null,
				worktree_path: "/tmp/wt",
				base_branch: "master",
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		render(<AgentPanel />);

		await screen.findByText(/1 running/);
	});
});

describe("RosterBadge availability state", () => {
	beforeAll(() => {
		Element.prototype.scrollIntoView = vi.fn();
	});

	beforeEach(() => {
		mockedAgents.getAgentConfig.mockResolvedValue(ROSTER);
		mockedAgents.listAgents.mockResolvedValue([]);
	});

	it("shows a green dot for an idle agent", async () => {
		render(<AgentPanel />);

		const dot = await screen.findByTestId("roster-dot-coder-1");
		// JSDOM normalizes #3fb950 to rgb(63, 185, 80)
		expect(dot.style.background).toBe("rgb(63, 185, 80)");
		expect(dot.style.animation).toBe("");
	});

	it("shows grey badge styling for an idle agent", async () => {
		render(<AgentPanel />);

		const badge = await screen.findByTestId("roster-badge-coder-1");
		// JSDOM normalizes #aaa18 to rgba(170, 170, 170, 0.094) and #aaa to rgb(170, 170, 170)
		expect(badge.style.background).toBe("rgba(170, 170, 170, 0.094)");
		expect(badge.style.color).toBe("rgb(170, 170, 170)");
	});

	// AC1: roster badge always shows idle (grey) even when agent is running
	it("shows a static green dot for a running agent (roster always idle)", async () => {
		const agentList: AgentInfo[] = [
			{
				story_id: "81_active",
				agent_name: "coder-1",
				status: "running",
				session_id: null,
				worktree_path: null,
				base_branch: null,
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		render(<AgentPanel />);

		const dot = await screen.findByTestId("roster-dot-coder-1");
		expect(dot.style.background).toBe("rgb(63, 185, 80)");
		// Roster is always idle — no pulsing animation
		expect(dot.style.animation).toBe("");
	});

	// AC1: roster badge always shows idle (grey) even when agent is running
	it("shows grey (idle) badge styling for a running agent", async () => {
		const agentList: AgentInfo[] = [
			{
				story_id: "81_active",
				agent_name: "coder-1",
				status: "running",
				session_id: null,
				worktree_path: null,
				base_branch: null,
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		render(<AgentPanel />);

		const badge = await screen.findByTestId("roster-badge-coder-1");
		// Always idle: grey background and grey text
		expect(badge.style.background).toBe("rgba(170, 170, 170, 0.094)");
		expect(badge.style.color).toBe("rgb(170, 170, 170)");
	});

	// AC2: after agent completes and returns to roster, badge shows idle
	it("shows idle state after agent status changes from running to completed", async () => {
		const agentList: AgentInfo[] = [
			{
				story_id: "81_completed",
				agent_name: "coder-1",
				status: "completed",
				session_id: null,
				worktree_path: null,
				base_branch: null,
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		render(<AgentPanel />);

		const badge = await screen.findByTestId("roster-badge-coder-1");
		const dot = screen.getByTestId("roster-dot-coder-1");
		// Completed agent: badge is idle
		expect(badge.style.background).toBe("rgba(170, 170, 170, 0.094)");
		expect(badge.style.color).toBe("rgb(170, 170, 170)");
		expect(dot.style.animation).toBe("");
	});
});

describe("Thinking traces hidden from agent stream UI", () => {
	beforeAll(() => {
		Element.prototype.scrollIntoView = vi.fn();
	});

	beforeEach(() => {
		mockedAgents.getAgentConfig.mockResolvedValue(ROSTER);
		mockedAgents.listAgents.mockResolvedValue([]);
		mockedSubscribeAgentStream.mockReturnValue(() => {});
	});

	// AC1: thinking block is never rendered even when thinking events arrive
	it("does not render thinking block when thinking event arrives", async () => {
		let emitEvent: ((e: AgentEvent) => void) | null = null;
		mockedSubscribeAgentStream.mockImplementation(
			(_storyId, _agentName, onEvent) => {
				emitEvent = onEvent;
				return () => {};
			},
		);

		const agentList: AgentInfo[] = [
			{
				story_id: "218_thinking",
				agent_name: "coder-1",
				status: "running",
				session_id: null,
				worktree_path: "/tmp/wt",
				base_branch: "master",
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		render(<AgentPanel />);
		await screen.findByTestId("roster-badge-coder-1");

		await act(async () => {
			emitEvent?.({
				type: "thinking",
				story_id: "218_thinking",
				agent_name: "coder-1",
				text: "Let me consider the problem carefully...",
			});
		});

		// AC1: thinking block must not be present
		expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
	});

	// AC2: after thinking events, only regular output is rendered
	it("renders regular output but not thinking block when both arrive", async () => {
		let emitEvent: ((e: AgentEvent) => void) | null = null;
		mockedSubscribeAgentStream.mockImplementation(
			(_storyId, _agentName, onEvent) => {
				emitEvent = onEvent;
				return () => {};
			},
		);

		const agentList: AgentInfo[] = [
			{
				story_id: "218_output",
				agent_name: "coder-1",
				status: "running",
				session_id: null,
				worktree_path: "/tmp/wt",
				base_branch: "master",
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		render(<AgentPanel />);
		await screen.findByTestId("roster-badge-coder-1");

		// Thinking event — must be ignored visually
		await act(async () => {
			emitEvent?.({
				type: "thinking",
				story_id: "218_output",
				agent_name: "coder-1",
				text: "thinking deeply",
			});
		});

		// AC3: output event still renders correctly (no regression)
		await act(async () => {
			emitEvent?.({
				type: "output",
				story_id: "218_output",
				agent_name: "coder-1",
				text: "Here is the result.",
			});
		});

		// AC1: no thinking block
		expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();

		// AC2+AC3: output area renders the text but NOT thinking text
		const outputArea = screen.getByTestId("agent-output-coder-1");
		expect(outputArea).toBeInTheDocument();
		expect(outputArea.textContent).toContain("Here is the result.");
		expect(outputArea.textContent).not.toContain("thinking deeply");
	});

	// AC3: output-only event stream (no thinking) still works
	it("renders output event text without a thinking block", async () => {
		let emitEvent: ((e: AgentEvent) => void) | null = null;
		mockedSubscribeAgentStream.mockImplementation(
			(_storyId, _agentName, onEvent) => {
				emitEvent = onEvent;
				return () => {};
			},
		);

		const agentList: AgentInfo[] = [
			{
				story_id: "218_noThink",
				agent_name: "coder-1",
				status: "running",
				session_id: null,
				worktree_path: "/tmp/wt",
				base_branch: "master",
				log_session_id: null,
			},
		];
		mockedAgents.listAgents.mockResolvedValue(agentList);

		render(<AgentPanel />);
		await screen.findByTestId("roster-badge-coder-1");

		await act(async () => {
			emitEvent?.({
				type: "output",
				story_id: "218_noThink",
				agent_name: "coder-1",
				text: "plain output line",
			});
		});

		expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
		const outputArea = screen.getByTestId("agent-output-coder-1");
		expect(outputArea.textContent).toContain("plain output line");
	});
});
