import { describe, it, expect, afterEach, vi } from "vitest";
import { render, screen, cleanup, waitFor, fireEvent } from "@testing-library/react";
import ProjectsPage from "@/app/projects/page";

// next/navigation 의 useRouter 를 목으로 대체해 인증 실패 시 리다이렉트 동작을 검증한다.
export const pushMock = vi.fn();

vi.mock("next/navigation", () => {
  return {
    __esModule: true,
    useRouter: () => ({ push: pushMock }),
  };
});

// ProjectsPage 프로젝트 목록 TDD
// - 마운트 시 /api/projects 로 GET 요청을 보내고, 반환된 프로젝트 목록을 테이블로 렌더링해야 한다.
// - 각 프로젝트 행에는 /editor?slug=..., /preview?slug=... 로 이동하는 "편집" / "미리보기" 링크가 있어야 한다.
// - 각 프로젝트 행에는 프로젝트를 삭제하는 "삭제" 버튼이 있고, 클릭 시 서버에 DELETE 요청을 보내고 목록/로컬스토리지를 갱신해야 한다.
// - 체크박스로 여러 프로젝트를 선택한 뒤 상단의 "선택 삭제" 버튼으로 일괄 삭제할 수 있어야 한다.

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

describe("ProjectsPage - 프로젝트 목록", () => {
  it("/api/projects 로부터 받은 프로젝트 목록을 제목/slug 기준으로 렌더링하고, 편집/미리보기 링크를 노출해야 한다", async () => {
    const projects = [
      {
        id: "1",
        title: "테스트 프로젝트 A",
        slug: "test-project-a",
        status: "DRAFT",
        createdAt: "2025-01-01T00:00:00.000Z",
        updatedAt: "2025-01-01T00:00:00.000Z",
      },
      {
        id: "2",
        title: "테스트 프로젝트 B",
        slug: "test-project-b",
        status: "PUBLISHED",
        createdAt: "2025-01-02T00:00:00.000Z",
        updatedAt: "2025-01-02T00:00:00.000Z",
      },
    ];

    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify(projects), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      }),
    );

    vi.stubGlobal("fetch", fetchMock);

    render(<ProjectsPage />);

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

    const [url, options] = fetchMock.mock.calls[0] as any;
    expect(url).toBe("/api/projects");
    expect(options).toBeUndefined();

    expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
    expect(screen.getByText("test-project-a")).toBeTruthy();

    expect(await screen.findByText("테스트 프로젝트 B")).toBeTruthy();
    expect(screen.getByText("test-project-b")).toBeTruthy();

    const editLinks = screen.getAllByText("편집");
    expect(editLinks).toHaveLength(2);
    expect(editLinks[0].closest("a")?.getAttribute("href")).toBe("/editor?slug=test-project-a");
    expect(editLinks[1].closest("a")?.getAttribute("href")).toBe("/editor?slug=test-project-b");

    const previewLinks = screen.getAllByText("미리보기");
    expect(previewLinks).toHaveLength(2);
    expect(previewLinks[0].closest("a")?.getAttribute("href")).toBe("/preview?slug=test-project-a");
    expect(previewLinks[1].closest("a")?.getAttribute("href")).toBe("/preview?slug=test-project-b");
  });

  it("삭제 버튼 클릭 시 서버에 DELETE 요청을 보내고 목록과 로컬스토리지를 갱신해야 한다", async () => {
    const projects = [
      {
        id: "1",
        title: "테스트 프로젝트 A",
        slug: "test-project-a",
        status: "DRAFT",
        createdAt: "2025-01-01T00:00:00.000Z",
        updatedAt: "2025-01-01T00:00:00.000Z",
      },
      {
        id: "2",
        title: "테스트 프로젝트 B",
        slug: "test-project-b",
        status: "PUBLISHED",
        createdAt: "2025-01-02T00:00:00.000Z",
        updatedAt: "2025-01-02T00:00:00.000Z",
      },
    ];

    window.localStorage.setItem("pb:project:test-project-a", "stored-project");
    window.localStorage.setItem("pb:autosave:test-project-a", "stored-autosave");

    const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
      const url = typeof input === "string" ? input : input.url;
      const method = init?.method ?? "GET";

      if (url === "/api/projects" && method === "GET") {
        return Promise.resolve(
          new Response(JSON.stringify(projects), {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }),
        );
      }

      if (url === "/api/projects/test-project-a" && method === "DELETE") {
        return Promise.resolve(new Response(null, { status: 200 }));
      }

      return Promise.resolve(new Response(null, { status: 500 }));
    });

    vi.stubGlobal("fetch", fetchMock);

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

    render(<ProjectsPage />);

    expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();

    const deleteButtons = screen.getAllByText("삭제");
    expect(deleteButtons.length).toBeGreaterThan(0);

    deleteButtons[0].click();

    await waitFor(() => {
      const deleteCall = fetchMock.mock.calls.find(([url, options]) => {
        return url === "/api/projects/test-project-a" && options?.method === "DELETE";
      });

      expect(deleteCall).toBeDefined();
    });

    expect(screen.queryByText("테스트 프로젝트 A")).toBeNull();
    expect(window.localStorage.getItem("pb:project:test-project-a")).toBeNull();
    expect(window.localStorage.getItem("pb:autosave:test-project-a")).toBeNull();

    confirmSpy.mockRestore();
  });

  it("체크박스로 여러 프로젝트를 선택한 뒤 '선택 삭제' 버튼으로 일괄 삭제할 수 있어야 한다", async () => {
    const projects = [
      {
        id: "1",
        title: "테스트 프로젝트 A",
        slug: "test-project-a",
        status: "DRAFT",
        createdAt: "2025-01-01T00:00:00.000Z",
        updatedAt: "2025-01-01T00:00:00.000Z",
      },
      {
        id: "2",
        title: "테스트 프로젝트 B",
        slug: "test-project-b",
        status: "PUBLISHED",
        createdAt: "2025-01-02T00:00:00.000Z",
        updatedAt: "2025-01-02T00:00:00.000Z",
      },
    ];

    window.localStorage.setItem("pb:project:test-project-a", "stored-project-a");
    window.localStorage.setItem("pb:autosave:test-project-a", "stored-autosave-a");
    window.localStorage.setItem("pb:project:test-project-b", "stored-project-b");
    window.localStorage.setItem("pb:autosave:test-project-b", "stored-autosave-b");

    const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
      const url = typeof input === "string" ? input : input.url;
      const method = init?.method ?? "GET";

      if (url === "/api/projects" && method === "GET") {
        return Promise.resolve(
          new Response(JSON.stringify(projects), {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }),
        );
      }

      if (url === "/api/projects/test-project-a" && method === "DELETE") {
        return Promise.resolve(new Response(null, { status: 200 }));
      }

      if (url === "/api/projects/test-project-b" && method === "DELETE") {
        return Promise.resolve(new Response(null, { status: 200 }));
      }

      return Promise.resolve(new Response(null, { status: 500 }));
    });

    vi.stubGlobal("fetch", fetchMock);

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

    render(<ProjectsPage />);

    expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
    expect(await screen.findByText("테스트 프로젝트 B")).toBeTruthy();

    const checkboxA = screen.getByLabelText("테스트 프로젝트 A 선택");
    const checkboxB = screen.getByLabelText("테스트 프로젝트 B 선택");

    fireEvent.click(checkboxA);
    fireEvent.click(checkboxB);

    const bulkDeleteButton = screen.getByText("선택 삭제");
    fireEvent.click(bulkDeleteButton);

    await waitFor(() => {
      const deleteCalls = fetchMock.mock.calls.filter(([, options]) => options?.method === "DELETE");
      const hasA = deleteCalls.some(([url]) => url === "/api/projects/test-project-a");
      const hasB = deleteCalls.some(([url]) => url === "/api/projects/test-project-b");

      expect(hasA).toBe(true);
      expect(hasB).toBe(true);

      // DELETE 요청이 성공적으로 처리된 뒤에는 목록에서 해당 프로젝트들이 사라져야 한다.
      expect(screen.queryByText("테스트 프로젝트 A")).toBeNull();
      expect(screen.queryByText("테스트 프로젝트 B")).toBeNull();
    });

    expect(window.localStorage.getItem("pb:project:test-project-a")).toBeNull();
    expect(window.localStorage.getItem("pb:autosave:test-project-a")).toBeNull();
    expect(window.localStorage.getItem("pb:project:test-project-b")).toBeNull();
    expect(window.localStorage.getItem("pb:autosave:test-project-b")).toBeNull();

    confirmSpy.mockRestore();
  });

  it("/api/projects 가 401 을 반환하면 로그인 페이지로 리다이렉트해야 한다", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ message: "프로젝트 목록을 조회하려면 로그인이 필요합니다." }), {
        status: 401,
        headers: { "Content-Type": "application/json" },
      }),
    );

    vi.stubGlobal("fetch", fetchMock);

    render(<ProjectsPage />);

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

    await waitFor(() => {
      expect(pushMock).toHaveBeenCalledWith("/login");
    });
  });

  it("헤더의 '로그아웃' 버튼을 클릭하면 /api/auth/logout 으로 POST 요청을 보내고 /login 으로 이동해야 한다", async () => {
    const projects = [
      {
        id: "1",
        title: "테스트 프로젝트 A",
        slug: "test-project-a",
        status: "DRAFT",
        createdAt: "2025-01-01T00:00:00.000Z",
        updatedAt: "2025-01-01T00:00:00.000Z",
      },
    ];

    const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
      const url = typeof input === "string" ? input : input.url;
      const method = init?.method ?? "GET";

      if (url === "/api/projects" && method === "GET") {
        return Promise.resolve(
          new Response(JSON.stringify(projects), {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }),
        );
      }

      if (url === "/api/auth/logout" && method === "POST") {
        return Promise.resolve(new Response(JSON.stringify({ message: "ok" }), { status: 200 }));
      }

      return Promise.resolve(new Response(null, { status: 500 }));
    });

    vi.stubGlobal("fetch", fetchMock);

    render(<ProjectsPage />);

    await waitFor(() => {
      expect(fetchMock).toHaveBeenCalled();
    });

    const logoutButton = await screen.findByRole("button", { name: "로그아웃" });
    fireEvent.click(logoutButton);

    await waitFor(() => {
      const logoutCall = fetchMock.mock.calls.find(([url, options]) => {
        return url === "/api/auth/logout" && options?.method === "POST";
      });

      expect(logoutCall).toBeDefined();
      expect(pushMock).toHaveBeenCalledWith("/login");
    });
  });

  it("/api/projects 가 500 을 반환하면 에러 메시지를 표시해야 한다", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response("server error", {
        status: 500,
        headers: { "Content-Type": "text/plain" },
      }),
    );

    vi.stubGlobal("fetch", fetchMock);

    render(<ProjectsPage />);

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

    expect(
      await screen.findByText(
        "프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
      ),
    ).toBeTruthy();
  });

  it("단일 프로젝트 삭제가 실패하면 에러 메시지를 표시해야 한다", async () => {
    const projects = [
      {
        id: "1",
        title: "삭제 실패 프로젝트",
        slug: "delete-fail-project",
        status: "DRAFT",
        createdAt: "2025-01-01T00:00:00.000Z",
        updatedAt: "2025-01-01T00:00:00.000Z",
      },
    ];

    const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
      const url = typeof input === "string" ? input : input.url;
      const method = init?.method ?? "GET";

      if (url === "/api/projects" && method === "GET") {
        return Promise.resolve(
          new Response(JSON.stringify(projects), {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }),
        );
      }

      if (url === "/api/projects/delete-fail-project" && method === "DELETE") {
        return Promise.resolve(new Response("fail", { status: 500 }));
      }

      return Promise.resolve(new Response(null, { status: 500 }));
    });

    vi.stubGlobal("fetch", fetchMock);

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

    render(<ProjectsPage />);

    expect(await screen.findByText("삭제 실패 프로젝트")).toBeTruthy();

    const deleteButton = screen.getByText("삭제");
    fireEvent.click(deleteButton);

    expect(
      await screen.findByText("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."),
    ).toBeTruthy();

    // 삭제 실패이므로 여전히 목록에 남아 있어야 한다.
    expect(screen.getByText("삭제 실패 프로젝트")).toBeTruthy();

    confirmSpy.mockRestore();
  });

  it("일괄 삭제에서 일부만 성공하면 에러 메시지를 표시해야 한다", async () => {
    const projects = [
      {
        id: "1",
        title: "성공 프로젝트",
        slug: "success-project",
        status: "DRAFT",
        createdAt: "2025-01-01T00:00:00.000Z",
        updatedAt: "2025-01-01T00:00:00.000Z",
      },
      {
        id: "2",
        title: "실패 프로젝트",
        slug: "fail-project",
        status: "DRAFT",
        createdAt: "2025-01-02T00:00:00.000Z",
        updatedAt: "2025-01-02T00:00:00.000Z",
      },
    ];

    const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
      const url = typeof input === "string" ? input : input.url;
      const method = init?.method ?? "GET";

      if (url === "/api/projects" && method === "GET") {
        return Promise.resolve(
          new Response(JSON.stringify(projects), {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }),
        );
      }

      if (url === "/api/projects/success-project" && method === "DELETE") {
        return Promise.resolve(new Response(null, { status: 200 }));
      }

      if (url === "/api/projects/fail-project" && method === "DELETE") {
        return Promise.resolve(new Response("fail", { status: 500 }));
      }

      return Promise.resolve(new Response(null, { status: 500 }));
    });

    vi.stubGlobal("fetch", fetchMock);

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

    render(<ProjectsPage />);

    expect(await screen.findByText("성공 프로젝트")).toBeTruthy();
    expect(await screen.findByText("실패 프로젝트")).toBeTruthy();

    const checkboxSuccess = screen.getByLabelText("성공 프로젝트 선택");
    const checkboxFail = screen.getByLabelText("실패 프로젝트 선택");

    fireEvent.click(checkboxSuccess);
    fireEvent.click(checkboxFail);

    const bulkDeleteButton = screen.getByText("선택 삭제");
    fireEvent.click(bulkDeleteButton);

    expect(
      await screen.findByText(
        "일부 프로젝트 삭제에 실패했습니다. 페이지를 새로고침한 뒤 목록을 확인해 주세요.",
      ),
    ).toBeTruthy();

    // 성공 프로젝트는 목록에서 사라지고, 실패 프로젝트는 남아 있어야 한다.
    expect(screen.queryByText("성공 프로젝트")).toBeNull();
    expect(screen.getByText("실패 프로젝트")).toBeTruthy();

    confirmSpy.mockRestore();
  });
});
