import { act, fireEvent, 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";

vi.mock("./api/gateway", () => ({
	gatewayApi: {
		getServerMode: vi.fn().mockResolvedValue({ mode: "standard" }),
	},
}));

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

vi.mock("./api/workflow", () => {
	return {
		workflowApi: {
			getAcceptance: vi.fn().mockResolvedValue({
				can_accept: false,
				reasons: [],
				warning: null,
				summary: { total: 0, passed: 0, failed: 0 },
				missing_categories: [],
			}),
			getReviewQueueAll: vi.fn().mockResolvedValue({ stories: [] }),
			getUpcomingStories: vi.fn().mockResolvedValue({ stories: [] }),
			recordTests: vi.fn(),
			ensureAcceptance: vi.fn(),
			getReviewQueue: vi.fn(),
			collectCoverage: vi.fn(),
			recordCoverage: vi.fn(),
			getStoryTodos: vi.fn().mockResolvedValue({ stories: [] }),
		},
	};
});

const mockedApi = vi.mocked(api);

describe("App", () => {
	beforeEach(() => {
		vi.resetModules();
		vi.clearAllMocks();
		mockedApi.getCurrentProject.mockResolvedValue(null);
		mockedApi.getKnownProjects.mockResolvedValue([]);
		mockedApi.getHomeDirectory.mockResolvedValue("/home/user");
		mockedApi.listDirectoryAbsolute.mockResolvedValue([]);
		mockedApi.getOllamaModels.mockResolvedValue([]);
		mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false);
		mockedApi.getAnthropicModels.mockResolvedValue([]);
		mockedApi.getModelPreference.mockResolvedValue(null);
		mockedApi.getOAuthStatus.mockResolvedValue({
			authenticated: false,
			expired: false,
			expires_at: 0,
			has_refresh_token: false,
		});
	});

	async function renderApp() {
		const { default: App } = await import("./App");
		let result!: ReturnType<typeof render>;
		await act(async () => {
			result = render(<App />);
		});
		return result;
	}

	it("calls getCurrentProject() on mount", async () => {
		await renderApp();

		await waitFor(() => {
			expect(mockedApi.getCurrentProject).toHaveBeenCalledTimes(1);
		});
	});

	it("skips selection screen and shows workspace when server already has a project open", async () => {
		mockedApi.getCurrentProject.mockResolvedValue("/home/user/myproject");

		await renderApp();

		await waitFor(() => {
			expect(
				screen.queryByPlaceholderText(/\/path\/to\/project/i),
			).not.toBeInTheDocument();
		});
	});

	it("renders the selection screen when no project is open", async () => {
		await renderApp();

		await waitFor(() => {
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});
	});

	it("populates path input with home directory", async () => {
		mockedApi.getHomeDirectory.mockResolvedValue("/Users/dave");

		await renderApp();

		await waitFor(() => {
			const input = screen.getByPlaceholderText(
				/\/path\/to\/project/i,
			) as HTMLInputElement;
			expect(input.value).toBe("/Users/dave/");
		});
	});

	it("opens project and shows chat view", async () => {
		mockedApi.openProject.mockResolvedValue("/home/user/myproject");

		await renderApp();

		await waitFor(() => {
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});

		const input = screen.getByPlaceholderText(
			/\/path\/to\/project/i,
		) as HTMLInputElement;
		await userEvent.clear(input);
		await userEvent.type(input, "/home/user/myproject");

		const openButton = screen.getByRole("button", { name: /open project/i });
		await userEvent.click(openButton);

		await waitFor(() => {
			expect(mockedApi.openProject).toHaveBeenCalledWith(
				"/home/user/myproject",
			);
		});
	});

	it("shows error when openProject fails", async () => {
		const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
		mockedApi.openProject.mockRejectedValue(new Error("Path does not exist"));

		await renderApp();

		await waitFor(() => {
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});

		const input = screen.getByPlaceholderText(
			/\/path\/to\/project/i,
		) as HTMLInputElement;
		await userEvent.clear(input);
		await userEvent.type(input, "/bad/path");

		const openButton = screen.getByRole("button", { name: /open project/i });
		await userEvent.click(openButton);

		await waitFor(() => {
			expect(screen.getByText(/Path does not exist/)).toBeInTheDocument();
		});
		errorSpy.mockRestore();
	});

	it("shows known projects list", async () => {
		mockedApi.getKnownProjects.mockResolvedValue([
			"/home/user/project1",
			"/home/user/project2",
		]);

		await renderApp();

		await waitFor(() => {
			expect(screen.getByTitle("/home/user/project1")).toBeInTheDocument();
			expect(screen.getByTitle("/home/user/project2")).toBeInTheDocument();
		});
	});

	it("shows error when path input is empty", async () => {
		await renderApp();

		await waitFor(() => {
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});

		const input = screen.getByPlaceholderText(
			/\/path\/to\/project/i,
		) as HTMLInputElement;
		await userEvent.clear(input);

		const openButton = screen.getByRole("button", { name: /open project/i });
		await userEvent.click(openButton);

		await waitFor(() => {
			expect(
				screen.getByText(/Please enter a project path/i),
			).toBeInTheDocument();
		});
	});

	it("calls forgetKnownProject and removes project from list", async () => {
		mockedApi.getKnownProjects.mockResolvedValue(["/home/user/project1"]);
		mockedApi.forgetKnownProject.mockResolvedValue(true);

		await renderApp();

		await waitFor(() => {
			expect(screen.getByTitle("/home/user/project1")).toBeInTheDocument();
		});

		const forgetButton = screen.getByRole("button", {
			name: /Forget project1/i,
		});
		await userEvent.click(forgetButton);

		await waitFor(() => {
			expect(mockedApi.forgetKnownProject).toHaveBeenCalledWith(
				"/home/user/project1",
			);
			expect(
				screen.queryByTitle("/home/user/project1"),
			).not.toBeInTheDocument();
		});
	});

	it("closes project and returns to selection screen", async () => {
		mockedApi.openProject.mockResolvedValue("/home/user/myproject");
		mockedApi.closeProject.mockResolvedValue(true);

		await renderApp();

		await waitFor(() => {
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});

		const input = screen.getByPlaceholderText(
			/\/path\/to\/project/i,
		) as HTMLInputElement;
		await userEvent.clear(input);
		await userEvent.type(input, "/home/user/myproject");

		const openButton = screen.getByRole("button", { name: /open project/i });
		await userEvent.click(openButton);

		await waitFor(() => {
			expect(mockedApi.openProject).toHaveBeenCalledWith(
				"/home/user/myproject",
			);
		});

		// Chat view should appear with close button
		const closeButton = await waitFor(() => screen.getByText("✕"));
		await userEvent.click(closeButton);

		await waitFor(() => {
			expect(mockedApi.closeProject).toHaveBeenCalled();
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});
	});

	it("handles ArrowDown and ArrowUp keyboard navigation when suggestions are visible", async () => {
		mockedApi.listDirectoryAbsolute.mockResolvedValue([
			{ name: "projects", kind: "dir" },
			{ name: "documents", kind: "dir" },
		]);

		await renderApp();

		// Wait for suggestions to appear after debounce
		await waitFor(
			() => {
				expect(screen.getByText(/projects\//)).toBeInTheDocument();
			},
			{ timeout: 2000 },
		);

		const input = screen.getByPlaceholderText(/\/path\/to\/project/i);

		// ArrowDown with matchList present — moves selection forward
		fireEvent.keyDown(input, { key: "ArrowDown" });

		// ArrowUp with matchList present — moves selection backward
		fireEvent.keyDown(input, { key: "ArrowUp" });
	});

	it("handles Tab keyboard navigation to accept suggestion", async () => {
		mockedApi.listDirectoryAbsolute.mockResolvedValue([
			{ name: "myrepo", kind: "dir" },
		]);

		await renderApp();

		await waitFor(
			() => {
				expect(screen.getByText(/myrepo\//)).toBeInTheDocument();
			},
			{ timeout: 2000 },
		);

		const input = screen.getByPlaceholderText(/\/path\/to\/project/i);

		// Tab with matchList present — accepts the selected match
		fireEvent.keyDown(input, { key: "Tab" });
	});

	it("handles Escape key to close suggestions", async () => {
		mockedApi.listDirectoryAbsolute.mockResolvedValue([
			{ name: "workspace", kind: "dir" },
		]);

		await renderApp();

		await waitFor(
			() => {
				expect(screen.getByText(/workspace\//)).toBeInTheDocument();
			},
			{ timeout: 2000 },
		);

		const input = screen.getByPlaceholderText(/\/path\/to\/project/i);

		// Escape closes suggestions
		fireEvent.keyDown(input, { key: "Escape" });

		await waitFor(() => {
			expect(screen.queryByText(/workspace\//)).not.toBeInTheDocument();
		});
	});

	it("sets document title to Huskies when no project is open", async () => {
		mockedApi.getCurrentProject.mockResolvedValue(null);

		await renderApp();

		await waitFor(() => {
			expect(document.title).toBe("Huskies");
		});
	});

	it("sets document title to project name when a project is open", async () => {
		mockedApi.getCurrentProject.mockResolvedValue("/home/user/reclaimer");

		await renderApp();

		await waitFor(() => {
			expect(document.title).toBe("reclaimer | Huskies");
		});
	});

	it("resets document title to Huskies after closing project", async () => {
		mockedApi.openProject.mockResolvedValue("/home/user/myproject");
		mockedApi.closeProject.mockResolvedValue(true);

		await renderApp();

		await waitFor(() => {
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});

		const input = screen.getByPlaceholderText(
			/\/path\/to\/project/i,
		) as HTMLInputElement;
		await userEvent.clear(input);
		await userEvent.type(input, "/home/user/myproject");

		const openButton = screen.getByRole("button", { name: /open project/i });
		await userEvent.click(openButton);

		await waitFor(() => {
			expect(document.title).toBe("myproject | Huskies");
		});

		const closeButton = await waitFor(() => screen.getByText("✕"));
		await userEvent.click(closeButton);

		await waitFor(() => {
			expect(document.title).toBe("Huskies");
		});
	});

	it("handles Enter key to trigger project open", async () => {
		mockedApi.openProject.mockResolvedValue("/home/user/myproject");

		await renderApp();

		await waitFor(() => {
			expect(
				screen.getByPlaceholderText(/\/path\/to\/project/i),
			).toBeInTheDocument();
		});

		const input = screen.getByPlaceholderText(
			/\/path\/to\/project/i,
		) as HTMLInputElement;
		await userEvent.clear(input);
		await userEvent.type(input, "/home/user/myproject");

		fireEvent.keyDown(input, { key: "Enter" });

		await waitFor(() => {
			expect(mockedApi.openProject).toHaveBeenCalledWith(
				"/home/user/myproject",
			);
		});
	});
});
