// ABOUTME: Unit tests for the Login page component.
// ABOUTME: Tests form rendering, validation, auth flow, and error handling.
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

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

// Mock PocketBase
const mockAuthWithPassword = vi.fn();
const mockAuthWithOAuth2 = vi.fn();
const mockListAuthMethods = vi.fn();
vi.mock("@/lib/pocketbase", () => ({
  pb: {
    collection: () => ({
      authWithPassword: mockAuthWithPassword,
      authWithOAuth2: mockAuthWithOAuth2,
      listAuthMethods: mockListAuthMethods,
    }),
  },
}));

import LoginPage from "./page";

describe("LoginPage", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Default: no OIDC configured, show email/password form
    mockListAuthMethods.mockResolvedValue({
      oauth2: {
        enabled: false,
        providers: [],
      },
    });
  });

  describe("rendering", () => {
    it("renders the login form with email and password inputs", async () => {
      render(<LoginPage />);

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

    it("renders a sign in button", async () => {
      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in/i }),
        ).toBeInTheDocument();
      });
    });

    it("renders the PhaseFlow branding", () => {
      render(<LoginPage />);

      expect(screen.getByText(/phaseflow/i)).toBeInTheDocument();
    });

    it("has email input with type email", async () => {
      render(<LoginPage />);

      await waitFor(() => {
        const emailInput = screen.getByLabelText(/email/i);
        expect(emailInput).toHaveAttribute("type", "email");
      });
    });

    it("has password input with type password", async () => {
      render(<LoginPage />);

      await waitFor(() => {
        const passwordInput = screen.getByLabelText(/password/i);
        expect(passwordInput).toHaveAttribute("type", "password");
      });
    });
  });

  describe("form submission", () => {
    it("calls PocketBase auth with email and password on submit", async () => {
      mockAuthWithPassword.mockResolvedValueOnce({ token: "test-token" });
      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.change(passwordInput, { target: { value: "password123" } });
      fireEvent.click(submitButton);

      await waitFor(() => {
        expect(mockAuthWithPassword).toHaveBeenCalledWith(
          "test@example.com",
          "password123",
        );
      });
    });

    it("redirects to dashboard on successful login", async () => {
      mockAuthWithPassword.mockResolvedValueOnce({ token: "test-token" });
      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.change(passwordInput, { target: { value: "password123" } });
      fireEvent.click(submitButton);

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

    it("shows loading state while authenticating", async () => {
      // Create a promise that we can control
      let resolveAuth: (value: unknown) => void = () => {};
      const authPromise = new Promise((resolve) => {
        resolveAuth = resolve;
      });
      mockAuthWithPassword.mockReturnValue(authPromise);

      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.change(passwordInput, { target: { value: "password123" } });
      fireEvent.click(submitButton);

      // Button should show loading state
      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /signing in/i }),
        ).toBeInTheDocument();
      });

      // Resolve the auth
      resolveAuth({ token: "test-token" });
    });

    it("disables form inputs while loading", async () => {
      let resolveAuth: (value: unknown) => void = () => {};
      const authPromise = new Promise((resolve) => {
        resolveAuth = resolve;
      });
      mockAuthWithPassword.mockReturnValue(authPromise);

      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.change(passwordInput, { target: { value: "password123" } });
      fireEvent.click(submitButton);

      await waitFor(() => {
        expect(emailInput).toBeDisabled();
        expect(passwordInput).toBeDisabled();
        expect(screen.getByRole("button")).toBeDisabled();
      });

      resolveAuth({ token: "test-token" });
    });
  });

  describe("error handling", () => {
    it("shows error message on failed login", async () => {
      mockAuthWithPassword.mockRejectedValueOnce(
        new Error("Invalid credentials"),
      );
      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.change(passwordInput, { target: { value: "wrongpassword" } });
      fireEvent.click(submitButton);

      await waitFor(() => {
        expect(screen.getByRole("alert")).toBeInTheDocument();
        expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
      });
    });

    it("clears error when user starts typing again", async () => {
      mockAuthWithPassword.mockRejectedValueOnce(
        new Error("Invalid credentials"),
      );
      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.change(passwordInput, { target: { value: "wrongpassword" } });
      fireEvent.click(submitButton);

      await waitFor(() => {
        expect(screen.getByRole("alert")).toBeInTheDocument();
      });

      // Start typing again
      fireEvent.change(emailInput, { target: { value: "new@example.com" } });

      expect(screen.queryByRole("alert")).not.toBeInTheDocument();
    });

    it("re-enables form after error", async () => {
      mockAuthWithPassword.mockRejectedValueOnce(
        new Error("Invalid credentials"),
      );
      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.change(passwordInput, { target: { value: "wrongpassword" } });
      fireEvent.click(submitButton);

      await waitFor(() => {
        expect(screen.getByRole("alert")).toBeInTheDocument();
      });

      // Form should be re-enabled
      expect(emailInput).not.toBeDisabled();
      expect(passwordInput).not.toBeDisabled();
      expect(
        screen.getByRole("button", { name: /sign in/i }),
      ).not.toBeDisabled();
    });
  });

  describe("validation", () => {
    it("does not submit with empty email", async () => {
      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
      });

      const passwordInput = screen.getByLabelText(/password/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(passwordInput, { target: { value: "password123" } });
      fireEvent.click(submitButton);

      // Should not call auth
      expect(mockAuthWithPassword).not.toHaveBeenCalled();
    });

    it("does not submit with empty password", async () => {
      render(<LoginPage />);

      // Wait for auth check to complete
      await waitFor(() => {
        expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
      });

      const emailInput = screen.getByLabelText(/email/i);
      const submitButton = screen.getByRole("button", { name: /sign in/i });

      fireEvent.change(emailInput, { target: { value: "test@example.com" } });
      fireEvent.click(submitButton);

      // Should not call auth
      expect(mockAuthWithPassword).not.toHaveBeenCalled();
    });
  });

  describe("OIDC authentication", () => {
    beforeEach(() => {
      // Configure OIDC provider available
      mockListAuthMethods.mockResolvedValue({
        oauth2: {
          enabled: true,
          providers: [
            {
              name: "oidc",
              displayName: "Pocket-ID",
              state: "test-state",
              codeVerifier: "test-verifier",
              codeChallenge: "test-challenge",
              codeChallengeMethod: "S256",
              authURL: "https://id.example.com/auth",
            },
          ],
        },
      });
    });

    it("shows OIDC button when provider is configured", async () => {
      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in with pocket-id/i }),
        ).toBeInTheDocument();
      });
    });

    it("hides email/password form when OIDC is available", async () => {
      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in with pocket-id/i }),
        ).toBeInTheDocument();
      });

      // Email/password form should not be visible
      expect(screen.queryByLabelText(/email/i)).not.toBeInTheDocument();
      expect(screen.queryByLabelText(/password/i)).not.toBeInTheDocument();
    });

    it("calls authWithOAuth2 when OIDC button is clicked", async () => {
      mockAuthWithOAuth2.mockResolvedValueOnce({ token: "test-token" });
      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in with pocket-id/i }),
        ).toBeInTheDocument();
      });

      const oidcButton = screen.getByRole("button", {
        name: /sign in with pocket-id/i,
      });
      fireEvent.click(oidcButton);

      await waitFor(() => {
        expect(mockAuthWithOAuth2).toHaveBeenCalledWith({ provider: "oidc" });
      });
    });

    it("redirects to dashboard on successful OIDC login", async () => {
      mockAuthWithOAuth2.mockResolvedValueOnce({
        token: "test-token",
        record: { id: "user-123" },
      });
      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in with pocket-id/i }),
        ).toBeInTheDocument();
      });

      const oidcButton = screen.getByRole("button", {
        name: /sign in with pocket-id/i,
      });
      fireEvent.click(oidcButton);

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

    it("shows loading state during OIDC authentication", async () => {
      let resolveAuth: (value: unknown) => void = () => {};
      const authPromise = new Promise((resolve) => {
        resolveAuth = resolve;
      });
      mockAuthWithOAuth2.mockReturnValue(authPromise);

      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in with pocket-id/i }),
        ).toBeInTheDocument();
      });

      const oidcButton = screen.getByRole("button", {
        name: /sign in with pocket-id/i,
      });
      fireEvent.click(oidcButton);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /signing in/i }),
        ).toBeInTheDocument();
        expect(screen.getByRole("button")).toBeDisabled();
      });

      resolveAuth({ token: "test-token" });
    });

    it("shows error message on OIDC failure", async () => {
      mockAuthWithOAuth2.mockRejectedValueOnce(
        new Error("Authentication cancelled"),
      );
      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in with pocket-id/i }),
        ).toBeInTheDocument();
      });

      const oidcButton = screen.getByRole("button", {
        name: /sign in with pocket-id/i,
      });
      fireEvent.click(oidcButton);

      await waitFor(() => {
        expect(screen.getByRole("alert")).toBeInTheDocument();
        expect(
          screen.getByText(/authentication cancelled/i),
        ).toBeInTheDocument();
      });
    });

    it("re-enables OIDC button after error", async () => {
      mockAuthWithOAuth2.mockRejectedValueOnce(
        new Error("Authentication cancelled"),
      );
      render(<LoginPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /sign in with pocket-id/i }),
        ).toBeInTheDocument();
      });

      const oidcButton = screen.getByRole("button", {
        name: /sign in with pocket-id/i,
      });
      fireEvent.click(oidcButton);

      await waitFor(() => {
        expect(screen.getByRole("alert")).toBeInTheDocument();
      });

      // Button should be re-enabled
      expect(
        screen.getByRole("button", { name: /sign in with pocket-id/i }),
      ).not.toBeDisabled();
    });
  });

  describe("fallback to email/password", () => {
    it("shows email/password form when OIDC is not configured", async () => {
      mockListAuthMethods.mockResolvedValue({
        oauth2: {
          enabled: false,
          providers: [],
        },
      });

      render(<LoginPage />);

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

    it("shows email/password form when listAuthMethods fails", async () => {
      mockListAuthMethods.mockRejectedValue(new Error("Network error"));

      render(<LoginPage />);

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

    it("does not show OIDC button when no providers configured", async () => {
      mockListAuthMethods.mockResolvedValue({
        oauth2: {
          enabled: false,
          providers: [],
        },
      });

      render(<LoginPage />);

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

      expect(
        screen.queryByRole("button", { name: /sign in with pocket-id/i }),
      ).not.toBeInTheDocument();
    });
  });
});
