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 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(() => {}));
    render(
      <MemoryRouter>
        <ProjectsPage />
      </MemoryRouter>
    );
    expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
  });

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

    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([]);
    render(
      <MemoryRouter>
        <ProjectsPage />
      </MemoryRouter>
    );

    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"));
    render(
      <MemoryRouter>
        <ProjectsPage />
      </MemoryRouter>
    );

    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]);

    render(
      <MemoryRouter>
        <ProjectsPage />
      </MemoryRouter>
    );

    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([]);

    render(
      <MemoryRouter>
        <ProjectsPage />
      </MemoryRouter>
    );

    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("opens edit dialog and saves changes", async () => {
    const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
    const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);

    render(
      <MemoryRouter>
        <ProjectsPage />
      </MemoryRouter>
    );

    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: /edit/i }));
    expect(screen.getByRole("dialog")).toBeInTheDocument();

    const nameInput = screen.getByDisplayValue("Alpha Project");
    fireEvent.change(nameInput, { target: { value: "Alpha Updated" } });
    fireEvent.click(screen.getByRole("button", { name: /save/i }));

    await waitFor(() => {
      expect(updateMock).toHaveBeenCalledWith("proj-1", {
        name: "Alpha Updated",
        description: "First project",
      });
    });
    expect(listMock).toHaveBeenCalledTimes(2);
  });

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

    render(
      <MemoryRouter>
        <ProjectsPage />
      </MemoryRouter>
    );

    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 }));
    expect(within(alphaCard).getByText(/are you sure/i)).toBeInTheDocument();

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

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