import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import AuthCallback from "@app/routes/AuthCallback";
import {
  POST_LOGIN_REDIRECT_STORAGE_KEY,
  springAuth,
} from "@app/auth/spring/springAuthClient";
import { expectConsole } from "@app/tests/failOnConsole";

// Mock springAuth; keep the real redirect-path helpers.
vi.mock("@app/auth/spring/springAuthClient", async () => {
  const actual = await vi.importActual<
    typeof import("@app/auth/spring/springAuthClient")
  >("@app/auth/spring/springAuthClient");
  return {
    ...actual,
    springAuth: {
      getSession: vi.fn(),
    },
  };
});

// Mock useNavigate
const mockNavigate = vi.fn();
vi.mock("react-router-dom", async () => {
  const actual = await vi.importActual("react-router-dom");
  return {
    ...actual,
    useNavigate: () => mockNavigate,
  };
});

describe("AuthCallback", () => {
  beforeEach(() => {
    localStorage.clear();
    sessionStorage.clear();
    vi.clearAllMocks();
    // Reset window.location.hash
    window.location.hash = "";
  });

  it("should extract JWT from URL hash and validate it", async () => {
    const mockToken = "oauth-jwt-token";
    const mockUser = {
      id: "123",
      email: "oauth@example.com",
      username: "oauthuser",
      role: "USER",
    };

    // Set URL hash with access token
    window.location.hash = `#access_token=${mockToken}`;

    // Mock successful session validation
    vi.mocked(springAuth.getSession).mockResolvedValueOnce({
      data: {
        session: {
          user: mockUser,
          access_token: mockToken,
          expires_in: 3600,
          expires_at: Date.now() + 3600000,
        },
      },
      error: null,
    });

    const dispatchEventSpy = vi.spyOn(window, "dispatchEvent");

    render(
      <BrowserRouter>
        <AuthCallback />
      </BrowserRouter>,
    );

    await waitFor(() => {
      // Verify JWT was stored
      expect(localStorage.getItem("stirling_jwt")).toBe(mockToken);

      // Verify jwt-available event was dispatched
      expect(dispatchEventSpy).toHaveBeenCalledWith(
        expect.objectContaining({ type: "jwt-available" }),
      );

      // Verify getSession was called to validate token
      expect(springAuth.getSession).toHaveBeenCalled();

      // Verify navigation to home
      expect(mockNavigate).toHaveBeenCalledWith("/", { replace: true });
    });
  });

  it("should redirect to login when no access token in hash", async () => {
    expectConsole.error(/\[AuthCallback\] No access_token in URL fragment/);
    // No hash or empty hash
    window.location.hash = "";

    render(
      <BrowserRouter>
        <AuthCallback />
      </BrowserRouter>,
    );

    await waitFor(() => {
      expect(mockNavigate).toHaveBeenCalledWith("/login", {
        replace: true,
        state: { error: "OAuth login failed - no token received." },
      });
      expect(localStorage.getItem("stirling_jwt")).toBeNull();
    });
  });

  it("should redirect to login when token validation fails", async () => {
    expectConsole.error(/\[AuthCallback\] Failed to validate token/);
    const invalidToken = "invalid-oauth-token";
    window.location.hash = `#access_token=${invalidToken}`;

    // Mock failed session validation
    vi.mocked(springAuth.getSession).mockResolvedValueOnce({
      data: { session: null },
      error: { message: "Invalid token" },
    });

    render(
      <BrowserRouter>
        <AuthCallback />
      </BrowserRouter>,
    );

    await waitFor(() => {
      // JWT should be stored initially
      expect(localStorage.getItem("stirling_jwt")).toBeNull(); // Cleared after validation failure

      // Verify redirect to login
      expect(mockNavigate).toHaveBeenCalledWith("/login", {
        replace: true,
        state: { error: "OAuth login failed - invalid token." },
      });
    });
  });

  it("should handle errors gracefully", async () => {
    expectConsole.error(/\[AuthCallback\] Authentication failed/);
    const mockToken = "error-token";
    window.location.hash = `#access_token=${mockToken}`;

    // Mock getSession throwing error
    vi.mocked(springAuth.getSession).mockRejectedValueOnce(
      new Error("Network error"),
    );

    render(
      <BrowserRouter>
        <AuthCallback />
      </BrowserRouter>,
    );

    await waitFor(() => {
      expect(mockNavigate).toHaveBeenCalledWith("/login", {
        replace: true,
        state: { error: "OAuth login failed. Please try again." },
      });
    });
  });

  it("should navigate to the stored post-login path when one is present", async () => {
    const mockToken = "oauth-jwt-token";
    const mockUser = {
      id: "123",
      email: "oauth@example.com",
      username: "oauthuser",
      role: "USER",
    };

    window.location.hash = `#access_token=${mockToken}`;
    sessionStorage.setItem(POST_LOGIN_REDIRECT_STORAGE_KEY, "/share/abc123");

    vi.mocked(springAuth.getSession).mockResolvedValueOnce({
      data: {
        session: {
          user: mockUser,
          access_token: mockToken,
          expires_in: 3600,
          expires_at: Date.now() + 3600000,
        },
      },
      error: null,
    });

    render(
      <BrowserRouter>
        <AuthCallback />
      </BrowserRouter>,
    );

    await waitFor(() => {
      expect(mockNavigate).toHaveBeenCalledWith("/share/abc123", {
        replace: true,
      });
    });
    // Stored path is single-use
    expect(sessionStorage.getItem(POST_LOGIN_REDIRECT_STORAGE_KEY)).toBeNull();
  });

  it("should fall back to home when the stored post-login path is unsafe", async () => {
    const mockToken = "oauth-jwt-token";
    const mockUser = {
      id: "123",
      email: "oauth@example.com",
      username: "oauthuser",
      role: "USER",
    };

    window.location.hash = `#access_token=${mockToken}`;
    // Protocol-relative URL — must not be followed
    sessionStorage.setItem(POST_LOGIN_REDIRECT_STORAGE_KEY, "//evil.example");

    vi.mocked(springAuth.getSession).mockResolvedValueOnce({
      data: {
        session: {
          user: mockUser,
          access_token: mockToken,
          expires_in: 3600,
          expires_at: Date.now() + 3600000,
        },
      },
      error: null,
    });

    render(
      <BrowserRouter>
        <AuthCallback />
      </BrowserRouter>,
    );

    await waitFor(() => {
      expect(mockNavigate).toHaveBeenCalledWith("/", { replace: true });
    });
    expect(sessionStorage.getItem(POST_LOGIN_REDIRECT_STORAGE_KEY)).toBeNull();
  });

  it("should display loading state while processing", () => {
    window.location.hash = "#access_token=processing-token";

    vi.mocked(springAuth.getSession).mockImplementationOnce(
      () =>
        new Promise((resolve) =>
          setTimeout(
            () =>
              resolve({
                data: { session: null },
                error: { message: "Token expired" },
              }),
            100,
          ),
        ),
    );

    const { getByText } = render(
      <BrowserRouter>
        <AuthCallback />
      </BrowserRouter>,
    );

    expect(getByText("Completing authentication")).toBeInTheDocument();
  });
});
