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 activity status indicator (Bug 140)", () => {
	beforeEach(() => {
		capturedWsHandlers = null;
		setupMocks();
	});

	it("shows activity label when tool activity fires during streaming content", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		// Simulate sending a message to set loading=true
		const input = screen.getByPlaceholderText("Send a message...");
		await act(async () => {
			fireEvent.change(input, { target: { value: "Read my file" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Simulate tokens arriving (streamingContent becomes non-empty)
		await act(async () => {
			capturedWsHandlers?.onToken("I'll read that file for you.");
		});

		// Now simulate a tool activity event while streamingContent is non-empty
		await act(async () => {
			capturedWsHandlers?.onActivity("read_file");
		});

		// The activity indicator should be visible with the tool activity label
		const indicator = await screen.findByTestId("activity-indicator");
		expect(indicator).toBeInTheDocument();
		expect(indicator).toHaveTextContent("Reading file...");
	});

	it("shows Thinking... fallback when loading with no streaming and no activity", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		// Simulate sending a message to set loading=true
		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 });
		});

		// No tokens, no activity — should show "Thinking..."
		const indicator = await screen.findByTestId("activity-indicator");
		expect(indicator).toBeInTheDocument();
		expect(indicator).toHaveTextContent("Thinking...");
	});

	it("hides Thinking... when streaming content is present but no tool activity", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		// Simulate sending a message to set loading=true
		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 });
		});

		// Tokens arrive — streamingContent is non-empty, no activity
		await act(async () => {
			capturedWsHandlers?.onToken("Here is my response...");
		});

		// The activity indicator should NOT be visible (just streaming bubble)
		expect(screen.queryByTestId("activity-indicator")).not.toBeInTheDocument();
	});

	it("shows activity label for Claude Code tool names (Read, Bash, etc.)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		// Simulate sending a message to set loading=true
		const input = screen.getByPlaceholderText("Send a message...");
		await act(async () => {
			fireEvent.change(input, { target: { value: "Read my file" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Simulate tokens arriving
		await act(async () => {
			capturedWsHandlers?.onToken("Let me read that.");
		});

		// Claude Code sends tool name "Read" (not "read_file")
		await act(async () => {
			capturedWsHandlers?.onActivity("Read");
		});

		const indicator = await screen.findByTestId("activity-indicator");
		expect(indicator).toBeInTheDocument();
		expect(indicator).toHaveTextContent("Reading file...");
	});

	it("shows activity label for Claude Code Bash tool", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");
		await act(async () => {
			fireEvent.change(input, { target: { value: "Run the tests" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		await act(async () => {
			capturedWsHandlers?.onToken("Running tests now.");
		});

		await act(async () => {
			capturedWsHandlers?.onActivity("Bash");
		});

		const indicator = await screen.findByTestId("activity-indicator");
		expect(indicator).toBeInTheDocument();
		expect(indicator).toHaveTextContent("Executing command...");
	});

	it("shows generic label for unknown tool names", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");
		await act(async () => {
			fireEvent.change(input, { target: { value: "Do something" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		await act(async () => {
			capturedWsHandlers?.onToken("Working on it.");
		});

		await act(async () => {
			capturedWsHandlers?.onActivity("SomeCustomTool");
		});

		const indicator = await screen.findByTestId("activity-indicator");
		expect(indicator).toBeInTheDocument();
		expect(indicator).toHaveTextContent("Using SomeCustomTool...");
	});
});

describe("Chat message queue (Story 155)", () => {
	beforeEach(() => {
		capturedWsHandlers = null;
		setupMocks();
	});

	it("shows queued message indicator when submitting while loading (AC1, AC2)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		// Send first message to put the chat in loading state
		const input = screen.getByPlaceholderText("Send a message...");
		await act(async () => {
			fireEvent.change(input, { target: { value: "First message" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Now type and submit a second message while loading is true
		await act(async () => {
			fireEvent.change(input, { target: { value: "Queued message" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// The queued message indicator should appear
		const indicator = await screen.findByTestId("queued-message-indicator");
		expect(indicator).toBeInTheDocument();
		expect(indicator).toHaveTextContent("Queued");
		expect(indicator).toHaveTextContent("Queued message");

		// Input should be cleared after queuing
		expect((input as HTMLTextAreaElement).value).toBe("");
	});

	it("auto-sends queued message when agent response completes (AC4)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");

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

		// Queue a second message while loading
		await act(async () => {
			fireEvent.change(input, { target: { value: "Auto-send this" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Verify it's queued
		expect(
			await screen.findByTestId("queued-message-indicator"),
		).toBeInTheDocument();

		// Simulate agent response completing (loading → false)
		await act(async () => {
			capturedWsHandlers?.onUpdate([
				{ role: "user", content: "First" },
				{ role: "assistant", content: "Done." },
			]);
		});

		// The queued indicator should disappear (message was sent)
		await waitFor(() => {
			expect(
				screen.queryByTestId("queued-message-indicator"),
			).not.toBeInTheDocument();
		});
	});

	it("cancel button discards the queued message (AC3, AC6)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");

		// Send first message to start loading
		await act(async () => {
			fireEvent.change(input, { target: { value: "First" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Queue a second message
		await act(async () => {
			fireEvent.change(input, { target: { value: "Discard me" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		const indicator = await screen.findByTestId("queued-message-indicator");
		expect(indicator).toBeInTheDocument();

		// Click the ✕ cancel button
		const cancelBtn = screen.getByTitle("Cancel queued message");
		await act(async () => {
			fireEvent.click(cancelBtn);
		});

		// Indicator should be gone
		expect(
			screen.queryByTestId("queued-message-indicator"),
		).not.toBeInTheDocument();
	});

	it("edit button puts queued message back into input (AC3)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");

		// Send first message to start loading
		await act(async () => {
			fireEvent.change(input, { target: { value: "First" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Queue a second message
		await act(async () => {
			fireEvent.change(input, { target: { value: "Edit me back" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		await screen.findByTestId("queued-message-indicator");

		// Click the Edit button
		const editBtn = screen.getByTitle("Edit queued message");
		await act(async () => {
			fireEvent.click(editBtn);
		});

		// Indicator should be gone and message back in input
		expect(
			screen.queryByTestId("queued-message-indicator"),
		).not.toBeInTheDocument();
		expect((input as HTMLTextAreaElement).value).toBe("Edit me back");
	});

	it("subsequent submissions are appended to the queue (Bug 168)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");

		// Send first message to start loading
		await act(async () => {
			fireEvent.change(input, { target: { value: "First" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

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

		await screen.findByTestId("queued-message-indicator");

		// Queue second message — should be appended, not overwrite the first
		await act(async () => {
			fireEvent.change(input, { target: { value: "Queue 2" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Both messages should be visible
		const indicators = await screen.findAllByTestId("queued-message-indicator");
		expect(indicators).toHaveLength(2);
		expect(indicators[0]).toHaveTextContent("Queue 1");
		expect(indicators[1]).toHaveTextContent("Queue 2");
	});

	it("all queued messages are drained at once when agent responds (Story 199)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");

		// Send first message to start loading
		await act(async () => {
			fireEvent.change(input, { target: { value: "First" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Queue two messages while loading
		await act(async () => {
			fireEvent.change(input, { target: { value: "Second" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});
		await act(async () => {
			fireEvent.change(input, { target: { value: "Third" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Both messages should be visible in order
		const indicators = await screen.findAllByTestId("queued-message-indicator");
		expect(indicators).toHaveLength(2);
		expect(indicators[0]).toHaveTextContent("Second");
		expect(indicators[1]).toHaveTextContent("Third");

		// Simulate first response completing — both "Second" and "Third" are drained at once
		await act(async () => {
			capturedWsHandlers?.onUpdate([
				{ role: "user", content: "First" },
				{ role: "assistant", content: "Response 1." },
			]);
		});

		// Both queued indicators should be gone — entire queue drained in one shot
		await waitFor(() => {
			const remaining = screen.queryAllByTestId("queued-message-indicator");
			expect(remaining).toHaveLength(0);
		});
	});

	it("does not auto-send queued message when generation is cancelled (AC6)", async () => {
		render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);

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

		const input = screen.getByPlaceholderText("Send a message...");

		// Send first message to start loading
		await act(async () => {
			fireEvent.change(input, { target: { value: "First" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Queue a second message
		await act(async () => {
			fireEvent.change(input, { target: { value: "Should not send" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		await screen.findByTestId("queued-message-indicator");

		// Click the stop button (■) — but input is empty so button is stop
		// Actually simulate cancel by clicking the stop button (which requires empty input)
		// We need to use the send button when input is empty (stop mode)
		// Simulate cancel via the cancelGeneration path: the button when loading && !input
		// At this point input is empty (was cleared after queuing)
		const stopButton = screen.getByRole("button", { name: "■" });
		await act(async () => {
			fireEvent.click(stopButton);
		});

		// Queued indicator should be gone (cancelled)
		await waitFor(() => {
			expect(
				screen.queryByTestId("queued-message-indicator"),
			).not.toBeInTheDocument();
		});
	});
});
