import {
	act,
	fireEvent,
	render,
	screen,
	waitFor,
} from "@testing-library/react";

import { beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "../api/client";
import type { Message } from "../types";
import { Chat } from "./Chat";

// Module-level store for the WebSocket handlers captured during connect().
type WsHandlers = {
	onToken: (content: string) => void;
	onUpdate: (history: Message[]) => void;
	onSessionId: (sessionId: string) => void;
	onError: (message: string) => void;
	onActivity: (toolName: string) => void;
	onReconciliationProgress: (
		storyId: string,
		status: string,
		message: string,
	) => void;
};
let capturedWsHandlers: WsHandlers | null = null;

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(),
		readFile: vi.fn(),
		listProjectFiles: vi.fn(),
		botCommand: vi.fn(),
	};
	class ChatWebSocket {
		connect(handlers: WsHandlers) {
			capturedWsHandlers = handlers;
		}
		close() {}
		sendChat() {}
		cancel() {}
	}
	return { api, ChatWebSocket };
});

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),
	readFile: vi.mocked(api.readFile),
	listProjectFiles: vi.mocked(api.listProjectFiles),
	botCommand: vi.mocked(api.botCommand),
};

function setupMocks() {
	mockedApi.getOllamaModels.mockResolvedValue(["llama3.1"]);
	mockedApi.getAnthropicApiKeyExists.mockResolvedValue(true);
	mockedApi.getAnthropicModels.mockResolvedValue([]);
	mockedApi.getModelPreference.mockResolvedValue(null);
	mockedApi.setModelPreference.mockResolvedValue(true);
	mockedApi.readFile.mockResolvedValue("");
	mockedApi.listProjectFiles.mockResolvedValue([]);
	mockedApi.cancelChat.mockResolvedValue(true);
	mockedApi.setAnthropicApiKey.mockResolvedValue(true);
	mockedApi.botCommand.mockResolvedValue({ response: "Bot response" });
}

describe("Chat two-column layout", () => {
	beforeEach(() => {
		capturedWsHandlers = null;
		setupMocks();
	});

	it("renders left and right column containers (AC1, AC2)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		expect(await screen.findByTestId("chat-content-area")).toBeInTheDocument();
		expect(await screen.findByTestId("chat-left-column")).toBeInTheDocument();
		expect(await screen.findByTestId("chat-right-column")).toBeInTheDocument();
	});

	it("renders chat input inside the left column (AC2, AC5)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		const leftColumn = await screen.findByTestId("chat-left-column");
		const input = screen.getByPlaceholderText("Send a message...");
		expect(leftColumn).toContainElement(input);
	});

	it("renders panels inside the right column (AC2)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		const rightColumn = await screen.findByTestId("chat-right-column");
		const agentsPanel = await screen.findByText("Agents");
		expect(rightColumn).toContainElement(agentsPanel);
	});

	it("uses row flex-direction on wide screens (AC3)", async () => {
		Object.defineProperty(window, "innerWidth", {
			writable: true,
			configurable: true,
			value: 1200,
		});
		window.dispatchEvent(new Event("resize"));

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

		const contentArea = await screen.findByTestId("chat-content-area");
		expect(contentArea).toHaveStyle({ flexDirection: "row" });
	});

	it("uses column flex-direction on narrow screens (AC4)", async () => {
		Object.defineProperty(window, "innerWidth", {
			writable: true,
			configurable: true,
			value: 600,
		});
		window.dispatchEvent(new Event("resize"));

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

		const contentArea = await screen.findByTestId("chat-content-area");
		expect(contentArea).toHaveStyle({ flexDirection: "column" });

		// Restore wide width for subsequent tests
		Object.defineProperty(window, "innerWidth", {
			writable: true,
			configurable: true,
			value: 1024,
		});
	});
});

describe("Chat input Shift+Enter behavior", () => {
	beforeEach(() => {
		capturedWsHandlers = null;
		setupMocks();
	});

	it("renders a textarea element for the chat input (AC3)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
		const input = screen.getByPlaceholderText("Send a message...");
		expect(input.tagName.toLowerCase()).toBe("textarea");
	});

	it("sends message on Enter key press without Shift (AC2)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
		const input = screen.getByPlaceholderText("Send a message...");

		await act(async () => {
			fireEvent.change(input, { target: { value: "Hello" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		await waitFor(() => {
			expect((input as HTMLTextAreaElement).value).toBe("");
		});
	});

	it("does not send message on Shift+Enter (AC1)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
		const input = screen.getByPlaceholderText("Send a message...");

		await act(async () => {
			fireEvent.change(input, { target: { value: "Hello" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: true });
		});

		expect((input as HTMLTextAreaElement).value).toBe("Hello");
	});
});

describe("Chat reconciliation banner", () => {
	beforeEach(() => {
		capturedWsHandlers = null;
		setupMocks();
	});

	it("shows banner when a non-done reconciliation event is received", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());

		await act(async () => {
			capturedWsHandlers?.onReconciliationProgress(
				"42_story_test",
				"checking",
				"Checking for committed work in 2_current/",
			);
		});

		expect(
			await screen.findByTestId("reconciliation-banner"),
		).toBeInTheDocument();
		expect(
			await screen.findByText("Reconciling startup state..."),
		).toBeInTheDocument();
	});

	it("shows event message in the banner", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());

		await act(async () => {
			capturedWsHandlers?.onReconciliationProgress(
				"42_story_test",
				"gates_running",
				"Running acceptance gates…",
			);
		});

		expect(
			await screen.findByText(/Running acceptance gates/),
		).toBeInTheDocument();
	});

	it("dismisses banner when done event is received", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());

		await act(async () => {
			capturedWsHandlers?.onReconciliationProgress(
				"42_story_test",
				"checking",
				"Checking for committed work",
			);
		});

		expect(
			await screen.findByTestId("reconciliation-banner"),
		).toBeInTheDocument();

		await act(async () => {
			capturedWsHandlers?.onReconciliationProgress(
				"",
				"done",
				"Startup reconciliation complete.",
			);
		});

		await waitFor(() => {
			expect(
				screen.queryByTestId("reconciliation-banner"),
			).not.toBeInTheDocument();
		});
	});
});
