import {
	cleanup,
	fireEvent,
	render,
	screen,
	waitFor,
	within,
} from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";

import { ProjectsPage } from "./ProjectsPage";
import * as projectsApi from "../api/projects";

const renderWithRouter = (ui: React.ReactElement) =>
	render(<MemoryRouter>{ui}</MemoryRouter>);

const mockProjects = [
	{
		id: "proj-1",
		name: "Alpha Project",
		description: "First project",
		owner_id: "user-1",
		default_ssh_key_id: null,
	},
	{
		id: "proj-2",
		name: "Beta Project",
		description: null,
		owner_id: "user-1",
		default_ssh_key_id: null,
	},
];

afterEach(() => {
	cleanup();
	vi.restoreAllMocks();
});

describe("ProjectsPage", () => {
	it("renders loading state initially", () => {
		vi.spyOn(projectsApi, "listProjects").mockImplementation(
			() => new Promise(() => {}),
		);
		renderWithRouter(<ProjectsPage />);
		expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
	});

	it("renders project list after loading", async () => {
		vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText("Alpha Project")).toBeInTheDocument();
		});
		expect(screen.getByText("Beta Project")).toBeInTheDocument();
		expect(screen.getByText("First project")).toBeInTheDocument();
	});

	it("renders empty state when no projects", async () => {
		vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
		});
	});

	it("renders error state with retry button", async () => {
		vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText(/failed to load projects/i)).toBeInTheDocument();
		});
		expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
	});

	it("opens create dialog and submits new project", async () => {
		const listMock = vi
			.spyOn(projectsApi, "listProjects")
			.mockResolvedValue([]);
		const createMock = vi
			.spyOn(projectsApi, "createProject")
			.mockResolvedValue(mockProjects[0]);

		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
		});

		fireEvent.click(screen.getByRole("button", { name: /new project/i }));
		expect(screen.getByRole("dialog")).toBeInTheDocument();

		fireEvent.change(screen.getByPlaceholderText(/project name/i), {
			target: { value: "Gamma Project" },
		});
		fireEvent.change(screen.getByPlaceholderText(/optional description/i), {
			target: { value: "A new project" },
		});
		fireEvent.click(screen.getByRole("button", { name: /create/i }));

		await waitFor(() => {
			expect(createMock).toHaveBeenCalledWith({
				name: "Gamma Project",
				description: "A new project",
			});
		});
		expect(listMock).toHaveBeenCalledTimes(2);
	});

	it("shows validation error when name is empty", async () => {
		vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);

		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
		});

		fireEvent.click(screen.getByRole("button", { name: /new project/i }));
		fireEvent.click(screen.getByRole("button", { name: /create/i }));

		expect(screen.getByText(/project name is required/i)).toBeInTheDocument();
	});

	it("renders settings link for each project", async () => {
		vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText("Alpha Project")).toBeInTheDocument();
		});

		const alphaCard = screen
			.getByText("Alpha Project")
			.closest(".project-card") as HTMLElement | null;
		if (!alphaCard) throw new Error("Card not found");

		const settingsLink = within(alphaCard).getByRole("link", {
			name: /settings/i,
		});
		expect(settingsLink).toBeInTheDocument();
		expect(settingsLink).toHaveAttribute("href", "/projects/proj-1/settings");
	});

	it("renders open workspace link as rightmost action", async () => {
		vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText("Alpha Project")).toBeInTheDocument();
		});

		const alphaCard = screen
			.getByText("Alpha Project")
			.closest(".project-card") as HTMLElement | null;
		if (!alphaCard) throw new Error("Card not found");

		const actions = alphaCard.querySelector(".project-actions");
		if (!actions) throw new Error("Actions container not found");

		const workspaceLink = within(alphaCard).getByRole("link", {
			name: /open workspace/i,
		});
		expect(workspaceLink).toBeInTheDocument();
		expect(workspaceLink).toHaveAttribute("href", "/projects/proj-1");

		// Verify it's the last action in the container
		const allActions = actions.querySelectorAll("a, button");
		const lastAction = allActions[allActions.length - 1];
		expect(lastAction).toBe(workspaceLink);
	});

	it("shows delete confirmation dialog and deletes project", async () => {
		const listMock = vi
			.spyOn(projectsApi, "listProjects")
			.mockResolvedValue(mockProjects);
		const deleteMock = vi
			.spyOn(projectsApi, "deleteProject")
			.mockResolvedValue(undefined);

		renderWithRouter(<ProjectsPage />);

		await waitFor(() => {
			expect(screen.getByText("Alpha Project")).toBeInTheDocument();
		});

		const alphaCard = screen
			.getByText("Alpha Project")
			.closest(".project-card") as HTMLElement | null;
		if (!alphaCard) throw new Error("Card not found");

		fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));

		const dialog = screen.getByRole("dialog");
		expect(dialog).toBeInTheDocument();
		expect(within(dialog).getByText(/delete workspace/i)).toBeInTheDocument();
		expect(within(dialog).getByText(/are you sure/i)).toBeInTheDocument();

		fireEvent.click(within(dialog).getByRole("button", { name: /delete/i }));

		await waitFor(() => {
			expect(deleteMock).toHaveBeenCalledWith("proj-1");
		});
		expect(listMock).toHaveBeenCalledTimes(2);
	});
});
