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

import { afterEach, 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;
// Captures the last sendChat call's arguments for assertion.
let lastSendChatArgs: { messages: Message[]; config: unknown } | 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(messages: Message[], config: unknown) {
			lastSendChatArgs = { messages, config };
		}
		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 localStorage persistence (Story 145)", () => {
	const PROJECT_PATH = "/tmp/project";
	const STORAGE_KEY = `storykit-chat-history:${PROJECT_PATH}`;

	beforeEach(() => {
		capturedWsHandlers = null;
		localStorage.clear();
		setupMocks();
	});

	afterEach(() => {
		localStorage.clear();
	});

	it("AC1: restores persisted messages on mount", async () => {
		const saved: Message[] = [
			{ role: "user", content: "Previously saved question" },
			{ role: "assistant", content: "Previously saved answer" },
		];
		localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));

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

		expect(
			await screen.findByText("Previously saved question"),
		).toBeInTheDocument();
		expect(
			await screen.findByText("Previously saved answer"),
		).toBeInTheDocument();
	});

	it("AC2: persists messages when WebSocket onUpdate fires", async () => {
		render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);

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

		const history: Message[] = [
			{ role: "user", content: "Hello" },
			{ role: "assistant", content: "Hi there!" },
		];

		await act(async () => {
			capturedWsHandlers?.onUpdate(history);
		});

		const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
		expect(stored).toEqual(history);
	});

	it("AC3: clears localStorage when New Session is clicked", async () => {
		const saved: Message[] = [
			{ role: "user", content: "Old message" },
			{ role: "assistant", content: "Old reply" },
		];
		localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));

		// Stub window.confirm to auto-approve the clear dialog
		const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);

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

		// Wait for the persisted message to appear
		expect(await screen.findByText("Old message")).toBeInTheDocument();

		// Click "New Session" button
		const newSessionBtn = screen.getByText(/New Session/);
		await act(async () => {
			fireEvent.click(newSessionBtn);
		});

		// localStorage should be cleared
		expect(localStorage.getItem(STORAGE_KEY)).toBeNull();

		// Messages should be gone from the UI
		expect(screen.queryByText("Old message")).not.toBeInTheDocument();

		confirmSpy.mockRestore();
	});

	it("Bug 245: messages survive unmount/remount cycle (page refresh)", async () => {
		// Step 1: Render Chat and populate messages via WebSocket onUpdate
		const { unmount } = render(
			<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />,
		);

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

		const history: Message[] = [
			{ role: "user", content: "Persist me across refresh" },
			{ role: "assistant", content: "I should survive a reload" },
		];

		await act(async () => {
			capturedWsHandlers?.onUpdate(history);
		});

		// Verify messages are persisted to localStorage
		expect(localStorage.getItem(STORAGE_KEY)).not.toBeNull();
		const storedBefore = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
		expect(storedBefore).toEqual(history);

		// Step 2: Unmount the Chat component (simulates page unload)
		unmount();

		// Verify localStorage was NOT cleared by unmount
		expect(localStorage.getItem(STORAGE_KEY)).not.toBeNull();
		const storedAfterUnmount = JSON.parse(
			localStorage.getItem(STORAGE_KEY) ?? "[]",
		);
		expect(storedAfterUnmount).toEqual(history);

		// Step 3: Remount the Chat component (simulates page reload)
		capturedWsHandlers = null;
		render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);

		// Verify messages are restored from localStorage
		expect(
			await screen.findByText("Persist me across refresh"),
		).toBeInTheDocument();
		expect(
			await screen.findByText("I should survive a reload"),
		).toBeInTheDocument();

		// Verify localStorage still has the messages
		const storedAfterRemount = JSON.parse(
			localStorage.getItem(STORAGE_KEY) ?? "[]",
		);
		expect(storedAfterRemount).toEqual(history);
	});

	it("Bug 245: after refresh, sendChat includes full prior history", async () => {
		// Step 1: Render, populate messages via onUpdate, then unmount (simulate refresh)
		const { unmount } = render(
			<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />,
		);
		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());

		const priorHistory: Message[] = [
			{ role: "user", content: "What is Rust?" },
			{ role: "assistant", content: "Rust is a systems programming language." },
		];
		await act(async () => {
			capturedWsHandlers?.onUpdate(priorHistory);
		});

		// Verify localStorage has the prior history
		const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
		expect(stored).toEqual(priorHistory);

		unmount();

		// Step 2: Remount (simulates page reload) — messages load from localStorage
		capturedWsHandlers = null;
		lastSendChatArgs = null;
		render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());

		// Verify prior messages are displayed
		expect(await screen.findByText("What is Rust?")).toBeInTheDocument();

		// Step 3: Send a new message — sendChat should include the full prior history
		const input = screen.getByPlaceholderText("Send a message...");
		await act(async () => {
			fireEvent.change(input, { target: { value: "Tell me more" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		// Verify sendChat was called with ALL prior messages + the new one
		expect(lastSendChatArgs).not.toBeNull();
		const args = lastSendChatArgs as unknown as {
			messages: Message[];
			config: unknown;
		};
		expect(args.messages).toHaveLength(3);
		expect(args.messages[0]).toEqual({
			role: "user",
			content: "What is Rust?",
		});
		expect(args.messages[1]).toEqual({
			role: "assistant",
			content: "Rust is a systems programming language.",
		});
		expect(args.messages[2]).toEqual({
			role: "user",
			content: "Tell me more",
		});
	});

	it("AC5: uses project-scoped storage key", async () => {
		const otherKey = "storykit-chat-history:/other/project";
		localStorage.setItem(
			otherKey,
			JSON.stringify([{ role: "user", content: "Other project msg" }]),
		);

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

		// Should NOT show the other project's messages
		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
		expect(screen.queryByText("Other project msg")).not.toBeInTheDocument();

		// Other project's data should still be in storage
		expect(localStorage.getItem(otherKey)).not.toBeNull();
	});
});

describe("Bug 264: Claude Code session ID persisted across browser refresh", () => {
	const PROJECT_PATH = "/tmp/project";
	const SESSION_KEY = `storykit-claude-session-id:${PROJECT_PATH}`;
	const STORAGE_KEY = `storykit-chat-history:${PROJECT_PATH}`;

	beforeEach(() => {
		capturedWsHandlers = null;
		lastSendChatArgs = null;
		localStorage.clear();
		setupMocks();
	});

	afterEach(() => {
		localStorage.clear();
	});

	it("AC1: session_id is persisted to localStorage when onSessionId fires", async () => {
		render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);

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

		await act(async () => {
			capturedWsHandlers?.onSessionId("test-session-abc");
		});

		await waitFor(() => {
			expect(localStorage.getItem(SESSION_KEY)).toBe("test-session-abc");
		});
	});

	it("AC2: after remount, next sendChat includes session_id from localStorage", async () => {
		// Step 1: Render, receive a session ID, then unmount (simulate refresh)
		localStorage.setItem(SESSION_KEY, "persisted-session-xyz");
		localStorage.setItem(
			STORAGE_KEY,
			JSON.stringify([
				{ role: "user", content: "Prior message" },
				{ role: "assistant", content: "Prior reply" },
			]),
		);

		const { unmount } = render(
			<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />,
		);
		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
		unmount();

		// Step 2: Remount (simulates page reload)
		capturedWsHandlers = null;
		lastSendChatArgs = null;
		render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());

		// Prior messages should be visible
		expect(await screen.findByText("Prior message")).toBeInTheDocument();

		// Step 3: Send a new message — config should include session_id
		const input = screen.getByPlaceholderText("Send a message...");
		await act(async () => {
			fireEvent.change(input, { target: { value: "Continue" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		expect(lastSendChatArgs).not.toBeNull();
		expect(
			(
				(
					lastSendChatArgs as unknown as {
						messages: Message[];
						config: unknown;
					}
				)?.config as Record<string, unknown>
			).session_id,
		).toBe("persisted-session-xyz");
	});

	it("AC3: clearing the session also clears the persisted session_id", async () => {
		localStorage.setItem(SESSION_KEY, "session-to-clear");

		const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);

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

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

		const newSessionBtn = screen.getByText(/New Session/);
		await act(async () => {
			fireEvent.click(newSessionBtn);
		});

		expect(localStorage.getItem(SESSION_KEY)).toBeNull();

		confirmSpy.mockRestore();
	});

	it("AC1: storage key is scoped to project path", async () => {
		const otherPath = "/other/project";
		const otherKey = `storykit-claude-session-id:${otherPath}`;
		localStorage.setItem(otherKey, "other-session");

		render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
		await waitFor(() => expect(capturedWsHandlers).not.toBeNull());

		await act(async () => {
			capturedWsHandlers?.onSessionId("my-session");
		});

		await waitFor(() => {
			expect(localStorage.getItem(SESSION_KEY)).toBe("my-session");
		});

		// Other project's session should be untouched
		expect(localStorage.getItem(otherKey)).toBe("other-session");
	});
});

describe("File reference expansion (Story 269 AC4)", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		capturedWsHandlers = null;
		lastSendChatArgs = null;
		setupMocks();
	});

	it("includes file contents as context when message contains @file reference", async () => {
		mockedApi.readFile.mockResolvedValue('fn main() { println!("hello"); }');

		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: "explain @src/main.rs" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		await waitFor(() => expect(lastSendChatArgs).not.toBeNull());
		const sentMessages = (
			lastSendChatArgs as NonNullable<typeof lastSendChatArgs>
		).messages;
		const userMsg = sentMessages[sentMessages.length - 1];
		expect(userMsg.content).toContain("explain @src/main.rs");
		expect(userMsg.content).toContain("[File: src/main.rs]");
		expect(userMsg.content).toContain("fn main()");
	});

	it("sends message without modification when no @file references are present", 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: "hello world" } });
		});
		await act(async () => {
			fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
		});

		await waitFor(() => expect(lastSendChatArgs).not.toBeNull());
		const sentMessages = (
			lastSendChatArgs as NonNullable<typeof lastSendChatArgs>
		).messages;
		const userMsg = sentMessages[sentMessages.length - 1];
		expect(userMsg.content).toBe("hello world");
		expect(mockedApi.readFile).not.toHaveBeenCalled();
	});
});
