import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "../api/client";
import type { ReviewStory } from "../api/workflow";
import { workflowApi } from "../api/workflow";
import { Chat } from "./Chat";

vi.mock("../api/client", () => {
	const api = {
		getOllamaModels: vi.fn(),
		getAnthropicApiKeyExists: vi.fn(),
		getAnthropicModels: vi.fn(),
		getModelPreference: vi.fn(),
		setModelPreference: vi.fn(),
		cancelChat: vi.fn(),
		setAnthropicApiKey: vi.fn(),
	};
	class ChatWebSocket {
		connect() {}
		close() {}
		sendChat() {}
		cancel() {}
	}
	return { api, ChatWebSocket };
});

vi.mock("../api/workflow", () => {
	return {
		workflowApi: {
			getAcceptance: vi.fn(),
			getReviewQueue: vi.fn(),
			getReviewQueueAll: vi.fn(),
			ensureAcceptance: vi.fn(),
			recordCoverage: vi.fn(),
			collectCoverage: vi.fn(),
		},
	};
});

const mockedApi = {
	getOllamaModels: vi.mocked(api.getOllamaModels),
	getAnthropicApiKeyExists: vi.mocked(api.getAnthropicApiKeyExists),
	getAnthropicModels: vi.mocked(api.getAnthropicModels),
	getModelPreference: vi.mocked(api.getModelPreference),
	setModelPreference: vi.mocked(api.setModelPreference),
	cancelChat: vi.mocked(api.cancelChat),
	setAnthropicApiKey: vi.mocked(api.setAnthropicApiKey),
};

const mockedWorkflow = {
	getAcceptance: vi.mocked(workflowApi.getAcceptance),
	getReviewQueue: vi.mocked(workflowApi.getReviewQueue),
	getReviewQueueAll: vi.mocked(workflowApi.getReviewQueueAll),
	ensureAcceptance: vi.mocked(workflowApi.ensureAcceptance),
};

describe("Chat review panel", () => {
	beforeEach(() => {
		mockedApi.getOllamaModels.mockResolvedValue(["llama3.1"]);
		mockedApi.getAnthropicApiKeyExists.mockResolvedValue(true);
		mockedApi.getAnthropicModels.mockResolvedValue([]);
		mockedApi.getModelPreference.mockResolvedValue(null);
		mockedApi.setModelPreference.mockResolvedValue(true);
		mockedApi.cancelChat.mockResolvedValue(true);
		mockedApi.setAnthropicApiKey.mockResolvedValue(true);

		mockedWorkflow.getAcceptance.mockResolvedValue({
			can_accept: false,
			reasons: ["No test results recorded for the story."],
			warning: null,
			summary: { total: 0, passed: 0, failed: 0 },
			missing_categories: ["unit", "integration"],
		});
		mockedWorkflow.getReviewQueueAll.mockResolvedValue({ stories: [] });
		mockedWorkflow.ensureAcceptance.mockResolvedValue(true);
	});

	it("shows an empty review queue state", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(
			await screen.findByText("Stories Awaiting Review"),
		).toBeInTheDocument();
		expect(await screen.findByText("0 ready / 0 total")).toBeInTheDocument();
		expect(
			await screen.findByText("No stories waiting for review."),
		).toBeInTheDocument();

		const updatedLabels = await screen.findAllByText(/Updated/i);
		expect(updatedLabels.length).toBeGreaterThanOrEqual(2);
	});

	it("renders review stories and proceeds", async () => {
		const story: ReviewStory = {
			story_id: "26_establish_tdd_workflow_and_gates",
			can_accept: true,
			reasons: [],
			warning: null,
			summary: { total: 3, passed: 3, failed: 0 },
			missing_categories: [],
		};

		mockedWorkflow.getReviewQueueAll
			.mockResolvedValueOnce({ stories: [story] })
			.mockResolvedValueOnce({ stories: [] });

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText(story.story_id)).toBeInTheDocument();

		const proceedButton = screen.getByRole("button", { name: "Proceed" });
		await userEvent.click(proceedButton);

		await waitFor(() => {
			expect(mockedWorkflow.ensureAcceptance).toHaveBeenCalledWith({
				story_id: story.story_id,
			});
		});

		expect(
			await screen.findByText("No stories waiting for review."),
		).toBeInTheDocument();
	});

	it("shows a review error when the queue fails to load", async () => {
		mockedWorkflow.getReviewQueueAll.mockRejectedValueOnce(
			new Error("Review queue failed"),
		);

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText(/Review queue failed/i)).toBeInTheDocument();
		expect(
			await screen.findByText(/Use Refresh to try again\./i),
		).toBeInTheDocument();
		expect(
			await screen.findByRole("button", { name: "Retry" }),
		).toBeInTheDocument();
	});

	it("refreshes the review queue when clicking refresh", async () => {
		mockedWorkflow.getReviewQueueAll
			.mockResolvedValueOnce({ stories: [] })
			.mockResolvedValueOnce({ stories: [] });

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		const refreshButtons = await screen.findAllByRole("button", {
			name: "Refresh",
		});
		const refreshButton = refreshButtons[0];

		await userEvent.click(refreshButton);

		await waitFor(() => {
			expect(mockedWorkflow.getReviewQueueAll).toHaveBeenCalled();
		});
	});

	it("disables proceed when a story is blocked", async () => {
		const story: ReviewStory = {
			story_id: "26_establish_tdd_workflow_and_gates",
			can_accept: false,
			reasons: ["Missing unit tests"],
			warning: null,
			summary: { total: 1, passed: 0, failed: 1 },
			missing_categories: ["unit"],
		};

		mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({
			stories: [story],
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText(story.story_id)).toBeInTheDocument();

		const blockedButton = screen.getByRole("button", { name: "Blocked" });
		expect(blockedButton).toBeDisabled();

		expect(await screen.findByText("Missing: unit")).toBeInTheDocument();
		expect(await screen.findByText("Missing unit tests")).toBeInTheDocument();
	});

	it("shows gate panel blocked status with reasons (AC1/AC3)", async () => {
		mockedWorkflow.getAcceptance.mockResolvedValueOnce({
			can_accept: false,
			reasons: ["No approved test plan for the story."],
			warning: null,
			summary: { total: 0, passed: 0, failed: 0 },
			missing_categories: ["unit", "integration"],
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText("Blocked")).toBeInTheDocument();
		expect(
			await screen.findByText("No approved test plan for the story."),
		).toBeInTheDocument();
		expect(
			await screen.findByText("Missing: unit, integration"),
		).toBeInTheDocument();
		expect(
			await screen.findByText(/0\/0 passing, 0 failing/),
		).toBeInTheDocument();
	});

	it("shows gate panel ready status when all tests pass (AC1/AC3)", async () => {
		mockedWorkflow.getAcceptance.mockResolvedValueOnce({
			can_accept: true,
			reasons: [],
			warning: null,
			summary: { total: 5, passed: 5, failed: 0 },
			missing_categories: [],
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText("Ready to accept")).toBeInTheDocument();
		expect(
			await screen.findByText(/5\/5 passing, 0 failing/),
		).toBeInTheDocument();
	});

	it("shows failing badge and count in review panel (AC4/AC5)", async () => {
		const story: ReviewStory = {
			story_id: "26_establish_tdd_workflow_and_gates",
			can_accept: false,
			reasons: ["3 tests are failing."],
			warning: "Multiple tests failing — fix one at a time.",
			summary: { total: 5, passed: 2, failed: 3 },
			missing_categories: [],
		};

		mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({
			stories: [story],
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText("Failing 3")).toBeInTheDocument();
		expect(await screen.findByText("Warning")).toBeInTheDocument();
		expect(
			await screen.findByText("Multiple tests failing — fix one at a time."),
		).toBeInTheDocument();
		expect(await screen.findByText("3 tests are failing.")).toBeInTheDocument();
		expect(
			await screen.findByText(/2\/5 passing, 3 failing/),
		).toBeInTheDocument();

		const blockedButton = screen.getByRole("button", { name: "Blocked" });
		expect(blockedButton).toBeDisabled();
	});

	it("shows gate warning when multiple tests fail (AC5)", async () => {
		mockedWorkflow.getAcceptance.mockResolvedValueOnce({
			can_accept: false,
			reasons: ["2 tests are failing."],
			warning: "Multiple tests failing — fix one at a time.",
			summary: { total: 4, passed: 2, failed: 2 },
			missing_categories: [],
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText("Blocked")).toBeInTheDocument();
		expect(
			await screen.findByText("Multiple tests failing — fix one at a time."),
		).toBeInTheDocument();
		expect(
			await screen.findByText(/2\/4 passing, 2 failing/),
		).toBeInTheDocument();
		expect(await screen.findByText("2 tests are failing.")).toBeInTheDocument();
	});

	it("does not call ensureAcceptance when clicking a blocked proceed button (AC4)", async () => {
		const story: ReviewStory = {
			story_id: "26_establish_tdd_workflow_and_gates",
			can_accept: false,
			reasons: ["Tests are failing."],
			warning: null,
			summary: { total: 3, passed: 1, failed: 2 },
			missing_categories: [],
		};

		mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({
			stories: [story],
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		const blockedButton = await screen.findByRole("button", {
			name: "Blocked",
		});
		expect(blockedButton).toBeDisabled();

		// Clear any prior calls then attempt click on disabled button
		mockedWorkflow.ensureAcceptance.mockClear();
		await userEvent.click(blockedButton);

		expect(mockedWorkflow.ensureAcceptance).not.toHaveBeenCalled();
	});

	it("shows proceed error when ensureAcceptance fails", async () => {
		const story: ReviewStory = {
			story_id: "26_establish_tdd_workflow_and_gates",
			can_accept: true,
			reasons: [],
			warning: null,
			summary: { total: 3, passed: 3, failed: 0 },
			missing_categories: [],
		};

		mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({
			stories: [story],
		});
		mockedWorkflow.ensureAcceptance.mockRejectedValueOnce(
			new Error("Acceptance blocked: tests still failing"),
		);

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		const proceedButton = await screen.findByRole("button", {
			name: "Proceed",
		});
		await userEvent.click(proceedButton);

		expect(
			await screen.findByText("Acceptance blocked: tests still failing"),
		).toBeInTheDocument();
	});

	it("shows gate error when acceptance endpoint fails", async () => {
		mockedWorkflow.getAcceptance.mockRejectedValueOnce(
			new Error("Server unreachable"),
		);

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText("Server unreachable")).toBeInTheDocument();

		const retryButtons = await screen.findAllByRole("button", {
			name: "Retry",
		});
		expect(retryButtons.length).toBeGreaterThanOrEqual(1);
	});

	it("refreshes gate status after proceeding on the current story", async () => {
		const story: ReviewStory = {
			story_id: "26_establish_tdd_workflow_and_gates",
			can_accept: true,
			reasons: [],
			warning: null,
			summary: { total: 2, passed: 2, failed: 0 },
			missing_categories: [],
		};

		mockedWorkflow.getAcceptance
			.mockResolvedValueOnce({
				can_accept: false,
				reasons: ["No test results recorded for the story."],
				warning: null,
				summary: { total: 0, passed: 0, failed: 0 },
				missing_categories: ["unit", "integration"],
			})
			.mockResolvedValueOnce({
				can_accept: true,
				reasons: [],
				warning: null,
				summary: { total: 2, passed: 2, failed: 0 },
				missing_categories: [],
			});

		mockedWorkflow.getReviewQueueAll
			.mockResolvedValueOnce({ stories: [story] })
			.mockResolvedValueOnce({ stories: [] });

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		const proceedButton = await screen.findByRole("button", {
			name: "Proceed",
		});
		await userEvent.click(proceedButton);

		await waitFor(() => {
			expect(mockedWorkflow.ensureAcceptance).toHaveBeenCalledWith({
				story_id: story.story_id,
			});
		});

		expect(await screen.findByText("Ready to accept")).toBeInTheDocument();
	});

	it("shows coverage below threshold in gate panel (AC3)", async () => {
		mockedWorkflow.getAcceptance.mockResolvedValueOnce({
			can_accept: false,
			reasons: ["Coverage below threshold (55.0% < 80.0%)."],
			warning: null,
			summary: { total: 3, passed: 3, failed: 0 },
			missing_categories: [],
			coverage_report: {
				current_percent: 55.0,
				threshold_percent: 80.0,
				baseline_percent: null,
			},
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText("Blocked")).toBeInTheDocument();
		expect(await screen.findByText(/Coverage: 55\.0%/)).toBeInTheDocument();
		expect(await screen.findByText(/threshold: 80\.0%/)).toBeInTheDocument();
		expect(
			await screen.findByText("Coverage below threshold (55.0% < 80.0%)."),
		).toBeInTheDocument();
	});

	it("shows coverage regression in review panel (AC4)", async () => {
		const story: ReviewStory = {
			story_id: "27_protect_tests_and_coverage",
			can_accept: false,
			reasons: ["Coverage regression: 90.0% → 82.0% (threshold: 80.0%)."],
			warning: null,
			summary: { total: 4, passed: 4, failed: 0 },
			missing_categories: [],
			coverage_report: {
				current_percent: 82.0,
				threshold_percent: 80.0,
				baseline_percent: 90.0,
			},
		};

		mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({
			stories: [story],
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(
			await screen.findByText(
				"Coverage regression: 90.0% → 82.0% (threshold: 80.0%).",
			),
		).toBeInTheDocument();
		expect(await screen.findByText(/Coverage: 82\.0%/)).toBeInTheDocument();
	});

	it("shows green coverage when above threshold (AC3)", async () => {
		mockedWorkflow.getAcceptance.mockResolvedValueOnce({
			can_accept: true,
			reasons: [],
			warning: null,
			summary: { total: 5, passed: 5, failed: 0 },
			missing_categories: [],
			coverage_report: {
				current_percent: 92.0,
				threshold_percent: 80.0,
				baseline_percent: 90.0,
			},
		});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByText("Ready to accept")).toBeInTheDocument();
		expect(await screen.findByText(/Coverage: 92\.0%/)).toBeInTheDocument();
	});

	it("collect coverage button triggers collection and refreshes gate", async () => {
		const mockedCollectCoverage = vi.mocked(workflowApi.collectCoverage);
		mockedCollectCoverage.mockResolvedValueOnce({
			current_percent: 85.0,
			threshold_percent: 80.0,
			baseline_percent: null,
		});

		mockedWorkflow.getAcceptance
			.mockResolvedValueOnce({
				can_accept: false,
				reasons: ["No test results recorded for the story."],
				warning: null,
				summary: { total: 0, passed: 0, failed: 0 },
				missing_categories: ["unit", "integration"],
			})
			.mockResolvedValueOnce({
				can_accept: true,
				reasons: [],
				warning: null,
				summary: { total: 5, passed: 5, failed: 0 },
				missing_categories: [],
				coverage_report: {
					current_percent: 85.0,
					threshold_percent: 80.0,
					baseline_percent: null,
				},
			});

		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		const collectButton = await screen.findByRole("button", {
			name: "Collect Coverage",
		});
		await userEvent.click(collectButton);

		await waitFor(() => {
			expect(mockedCollectCoverage).toHaveBeenCalledWith({
				story_id: "26_establish_tdd_workflow_and_gates",
			});
		});

		expect(await screen.findByText(/Coverage: 85\.0%/)).toBeInTheDocument();
	});
});
