import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { AuthGate } from "@/components/auth/auth-gate";
import { AUTH_TOKEN_KEY } from "@/lib/auth/client";

const identity = {
  id: "00000000-0000-4000-8000-000000000042",
  externalUserId: "42",
  login: "alice",
  fullName: "Alice Smith",
  position: "Engineer",
  email: "alice@example.com",
  firstName: "Alice",
  lastName: "Smith",
  middleName: "Jane",
  roles: ["admin", "employee"],
  lastModelId: "chatgpt" as const,
};

function jsonResponse(body: unknown, init?: ResponseInit): Response {
  return new Response(JSON.stringify(body), {
    headers: { "Content-Type": "application/json" },
    ...init,
  });
}

describe("AuthGate", () => {
  let tokens: Map<string, string>;

  beforeEach(() => {
    tokens = new Map();
    vi.stubGlobal("localStorage", {
      clear: vi.fn(() => tokens.clear()),
      getItem: vi.fn((key: string) => tokens.get(key) ?? null),
      removeItem: vi.fn((key: string) => tokens.delete(key)),
      setItem: vi.fn((key: string, value: string) => tokens.set(key, value)),
    });
    vi.stubGlobal("fetch", vi.fn());
  });

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

  it("never renders login while a stored token is being validated", () => {
    tokens.set(AUTH_TOKEN_KEY, "valid-token");
    vi.mocked(fetch).mockReturnValue(new Promise(() => undefined));

    render(<AuthGate />);

    expect(screen.getByLabelText("Проверяем доступ")).toBeInTheDocument();
    expect(screen.getByText("Проверяем доступ")).toBeInTheDocument();
    expect(
      screen.queryByRole("heading", { name: "Вход" }),
    ).not.toBeInTheDocument();
  });

  it("opens the chat after a valid identity response", async () => {
    tokens.set(AUTH_TOKEN_KEY, "valid-token");
    vi.mocked(fetch).mockResolvedValue(
      jsonResponse({
        user: identity,
      }),
    );

    render(<AuthGate />);

    expect(await screen.findByText("Новый чат")).toBeInTheDocument();
  });

  it("rejects an identity with an invalid model slug", async () => {
    tokens.set(AUTH_TOKEN_KEY, "valid-token");
    vi.mocked(fetch).mockResolvedValue(
      jsonResponse({ user: { ...identity, lastModelId: "GPT-4" } }),
    );

    render(<AuthGate />);

    expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
  });

  it("sends the stored token with every identity request", async () => {
    tokens.set(AUTH_TOKEN_KEY, "valid-token");
    vi.mocked(fetch).mockResolvedValue(
      jsonResponse({
        user: identity,
      }),
    );

    render(<AuthGate />);

    await screen.findByText("Новый чат");
    const [, init] = vi.mocked(fetch).mock.calls[0];
    expect(init?.cache).toBe("no-store");
    expect(new Headers(init?.headers).get("Authorization")).toBe(
      "Bearer valid-token",
    );
  });

  it("shows login when no token is stored", async () => {
    render(<AuthGate />);

    expect(screen.getByLabelText("Проверяем доступ")).toBeInTheDocument();
    expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
    expect(vi.mocked(fetch)).not.toHaveBeenCalled();
  });

  it("clears an invalid token and shows login", async () => {
    localStorage.setItem(AUTH_TOKEN_KEY, "invalid-token");
    vi.mocked(fetch).mockResolvedValue(jsonResponse({ error: "Unauthorized" }, { status: 401 }));

    render(<AuthGate />);

    expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
    expect(tokens.get(AUTH_TOKEN_KEY)).toBeUndefined();
  });

  it("stores the token and opens chat after successful login", async () => {
    const user = userEvent.setup();
    vi.mocked(fetch).mockResolvedValue(
      jsonResponse({
        token: "new-token",
        user: identity,
      }),
    );

    render(<AuthGate />);

    await screen.findByRole("heading", { name: "Вход" });
    await user.type(screen.getByLabelText("Логин или почта"), "alice");
    await user.type(screen.getByLabelText("Пароль"), "Demo1234!");
    await user.click(screen.getByRole("button", { name: "Продолжить" }));

    expect(await screen.findByText("Новый чат")).toBeInTheDocument();
    expect(tokens.get(AUTH_TOKEN_KEY)).toBe("new-token");
  });

  it("removes the token when the authenticated user logs out", async () => {
    const user = userEvent.setup();
    tokens.set(AUTH_TOKEN_KEY, "valid-token");
    vi.mocked(fetch).mockResolvedValue(
      jsonResponse({
        user: identity,
      }),
    );

    render(<AuthGate />);

    await user.click(await screen.findByRole("button", { name: "Выйти" }));

    expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
    expect(tokens.get(AUTH_TOKEN_KEY)).toBeUndefined();
  });

  it("ends the authenticated session when the chat API returns 401", async () => {
    const user = userEvent.setup();
    tokens.set(AUTH_TOKEN_KEY, "expired-token");
    vi.mocked(fetch)
      .mockResolvedValueOnce(
        jsonResponse({
          user: identity,
        }),
      )
      .mockResolvedValueOnce(
        jsonResponse({ error: "Unauthorized" }, { status: 401 }),
      );

    render(<AuthGate />);

    await user.type(await screen.findByLabelText("Сообщение"), "Привет{enter}");

    expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
    expect(tokens.get(AUTH_TOKEN_KEY)).toBeUndefined();
    expect(screen.queryByLabelText("Сообщение")).not.toBeInTheDocument();
  });
});
