import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { AuthConfigResponse } from "@mosaic/shared";
import LoginPage from "./page";

/* ------------------------------------------------------------------ */
/*  Hoisted mocks                                                      */
/* ------------------------------------------------------------------ */

const { mockOAuth2, mockSignInEmail, mockPush } = vi.hoisted(() => ({
  mockOAuth2: vi.fn(),
  mockSignInEmail: vi.fn(),
  mockPush: vi.fn(),
}));

vi.mock("next/navigation", () => ({
  useRouter: (): { push: Mock } => ({
    push: mockPush,
  }),
}));

vi.mock("@/lib/auth-client", () => ({
  signIn: {
    oauth2: mockOAuth2,
    email: mockSignInEmail,
  },
}));

vi.mock("@/lib/config", () => ({
  API_BASE_URL: "http://localhost:3001",
}));

/* ------------------------------------------------------------------ */
/*  Helpers                                                             */
/* ------------------------------------------------------------------ */

function mockFetchConfig(config: AuthConfigResponse): void {
  (global.fetch as Mock).mockResolvedValueOnce({
    ok: true,
    json: (): Promise<AuthConfigResponse> => Promise.resolve(config),
  });
}

function mockFetchFailure(): void {
  (global.fetch as Mock).mockRejectedValueOnce(new Error("Network error"));
}

const OAUTH_ONLY_CONFIG: AuthConfigResponse = {
  providers: [{ id: "authentik", name: "Authentik", type: "oauth" }],
};

const EMAIL_ONLY_CONFIG: AuthConfigResponse = {
  providers: [{ id: "email", name: "Email", type: "credentials" }],
};

const BOTH_PROVIDERS_CONFIG: AuthConfigResponse = {
  providers: [
    { id: "authentik", name: "Authentik", type: "oauth" },
    { id: "email", name: "Email", type: "credentials" },
  ],
};

/* ------------------------------------------------------------------ */
/*  Tests                                                               */
/* ------------------------------------------------------------------ */

describe("LoginPage", (): void => {
  beforeEach((): void => {
    vi.clearAllMocks();
    global.fetch = vi.fn();
  });

  it("renders loading state initially", (): void => {
    // Never resolve fetch so it stays in loading state
    // eslint-disable-next-line @typescript-eslint/no-empty-function -- intentionally never-resolving promise to test loading state
    (global.fetch as Mock).mockReturnValueOnce(new Promise(() => {}));

    render(<LoginPage />);

    expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
    expect(screen.getByText("Loading authentication options")).toBeInTheDocument();
  });

  it("renders the page heading and description", (): void => {
    mockFetchConfig(EMAIL_ONLY_CONFIG);

    render(<LoginPage />);

    expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Welcome to Mosaic Stack");
    expect(screen.getByText(/Your personal assistant platform/i)).toBeInTheDocument();
  });

  it("has proper layout styling", (): void => {
    mockFetchConfig(EMAIL_ONLY_CONFIG);

    const { container } = render(<LoginPage />);
    const main = container.querySelector("main");
    expect(main).toHaveClass("flex", "min-h-screen");
  });

  it("fetches /auth/config on mount", async (): Promise<void> => {
    mockFetchConfig(EMAIL_ONLY_CONFIG);

    render(<LoginPage />);

    await waitFor((): void => {
      expect(global.fetch).toHaveBeenCalledWith("http://localhost:3001/auth/config");
    });
  });

  it("renders OAuth button when OIDC provider is in config", async (): Promise<void> => {
    mockFetchConfig(OAUTH_ONLY_CONFIG);

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByRole("button", { name: /continue with authentik/i })).toBeInTheDocument();
    });
  });

  it("renders only LoginForm when only email provider is configured", async (): Promise<void> => {
    mockFetchConfig(EMAIL_ONLY_CONFIG);

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: /continue with/i })).not.toBeInTheDocument();
  });

  it("renders both OAuth button and LoginForm with divider when both providers present", async (): Promise<void> => {
    mockFetchConfig(BOTH_PROVIDERS_CONFIG);

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByRole("button", { name: /continue with authentik/i })).toBeInTheDocument();
    });

    expect(screen.getByText(/or continue with email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
  });

  it("does not render divider when only OAuth providers present", async (): Promise<void> => {
    mockFetchConfig(OAUTH_ONLY_CONFIG);

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByRole("button", { name: /continue with authentik/i })).toBeInTheDocument();
    });

    expect(screen.queryByText(/or continue with email/i)).not.toBeInTheDocument();
  });

  it("falls back to email-only on fetch failure", async (): Promise<void> => {
    mockFetchFailure();

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: /continue with/i })).not.toBeInTheDocument();
  });

  it("falls back to email-only on non-ok response", async (): Promise<void> => {
    (global.fetch as Mock).mockResolvedValueOnce({
      ok: false,
      status: 500,
    });

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    expect(screen.queryByRole("button", { name: /continue with/i })).not.toBeInTheDocument();
  });

  it("calls signIn.oauth2 when OAuth button is clicked", async (): Promise<void> => {
    mockFetchConfig(OAUTH_ONLY_CONFIG);
    const user = userEvent.setup();

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByRole("button", { name: /continue with authentik/i })).toBeInTheDocument();
    });

    await user.click(screen.getByRole("button", { name: /continue with authentik/i }));

    expect(mockOAuth2).toHaveBeenCalledWith({
      providerId: "authentik",
      callbackURL: "/",
    });
  });

  it("calls signIn.email and redirects on success", async (): Promise<void> => {
    mockFetchConfig(EMAIL_ONLY_CONFIG);
    mockSignInEmail.mockResolvedValueOnce({ data: { user: {} } });
    const user = userEvent.setup();

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    await user.type(screen.getByLabelText(/email/i), "test@example.com");
    await user.type(screen.getByLabelText(/password/i), "password123");
    await user.click(screen.getByRole("button", { name: /continue/i }));

    await waitFor((): void => {
      expect(mockSignInEmail).toHaveBeenCalledWith({
        email: "test@example.com",
        password: "password123",
      });
    });

    await waitFor((): void => {
      expect(mockPush).toHaveBeenCalledWith("/tasks");
    });
  });

  it("shows error banner on sign-in failure", async (): Promise<void> => {
    mockFetchConfig(EMAIL_ONLY_CONFIG);
    mockSignInEmail.mockResolvedValueOnce({
      error: { message: "Invalid credentials" },
    });
    const user = userEvent.setup();

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    await user.type(screen.getByLabelText(/email/i), "test@example.com");
    await user.type(screen.getByLabelText(/password/i), "wrong");
    await user.click(screen.getByRole("button", { name: /continue/i }));

    await waitFor((): void => {
      expect(screen.getByText("Invalid credentials")).toBeInTheDocument();
    });

    expect(mockPush).not.toHaveBeenCalled();
  });

  it("shows generic error on unexpected sign-in exception", async (): Promise<void> => {
    mockFetchConfig(EMAIL_ONLY_CONFIG);
    mockSignInEmail.mockRejectedValueOnce(new Error("Network failure"));
    const user = userEvent.setup();

    render(<LoginPage />);

    await waitFor((): void => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    await user.type(screen.getByLabelText(/email/i), "test@example.com");
    await user.type(screen.getByLabelText(/password/i), "password");
    await user.click(screen.getByRole("button", { name: /continue/i }));

    await waitFor((): void => {
      expect(
        screen.getByText("Something went wrong. Please try again in a moment.")
      ).toBeInTheDocument();
    });
  });
});
