/**
 * @jest-environment node
 *
 * Clerk Auth Helpers Tests
 * Tests getClerkUser, getClerkUserId, and requireClerkAuth from lib/clerk.ts.
 *
 * @clerk/nextjs/server is fully mocked — no real Clerk credentials required.
 */

// Must mock @clerk/nextjs/server before any imports that use it
const mockAuth = jest.fn();
const mockCurrentUser = jest.fn();
jest.mock("@clerk/nextjs/server", () => ({
  auth: () => mockAuth(),
  currentUser: () => mockCurrentUser(),
}));

import {
  getClerkUser,
  getClerkUserId,
  requireClerkAuth,
  type ClerkAuthUser,
} from "@/lib/clerk";

// ── Env helpers ─────────────────────────────────────────────────────────────

const CLERK_KEY = "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY";

function setClerkConfigured() {
  process.env[CLERK_KEY] = "pk_test_xxx";
}

function clearClerkConfig() {
  delete process.env[CLERK_KEY];
}

// Save & restore env after each test
const originalEnv = { ...process.env };
afterEach(() => {
  jest.clearAllMocks();
  // Restore original env state
  delete process.env[CLERK_KEY];
  if (CLERK_KEY in originalEnv) {
    process.env[CLERK_KEY] = originalEnv[CLERK_KEY];
  }
});

// ── Fixtures ─────────────────────────────────────────────────────────────────

const fakeClerkUser = {
  id: "user_clerk_123",
  primaryEmailAddressId: "email_1",
  emailAddresses: [{ id: "email_1", emailAddress: "ahmed@mawidi.com" }],
  fullName: "Ahmed Al-Rashid",
  firstName: "Ahmed",
  imageUrl: "https://img.clerk.com/ahmed.jpg",
};

// ── getClerkUser ─────────────────────────────────────────────────────────────

describe("getClerkUser", () => {
  it("returns null when NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is not set", async () => {
    clearClerkConfig();
    const result = await getClerkUser();
    expect(result).toBeNull();
    expect(mockCurrentUser).not.toHaveBeenCalled();
  });

  it("returns null when currentUser() returns null (no active session)", async () => {
    setClerkConfigured();
    mockCurrentUser.mockResolvedValue(null);
    const result = await getClerkUser();
    expect(result).toBeNull();
  });

  it("maps Clerk user to ClerkAuthUser shape", async () => {
    setClerkConfigured();
    mockCurrentUser.mockResolvedValue(fakeClerkUser);

    const result = await getClerkUser();

    expect(result).toEqual<ClerkAuthUser>({
      id: "user_clerk_123",
      email: "ahmed@mawidi.com",
      name: "Ahmed Al-Rashid",
      imageUrl: "https://img.clerk.com/ahmed.jpg",
    });
  });

  it("falls back to firstName when fullName is null", async () => {
    setClerkConfigured();
    mockCurrentUser.mockResolvedValue({
      ...fakeClerkUser,
      fullName: null,
      firstName: "Ahmed",
    });

    const result = await getClerkUser();
    expect(result?.name).toBe("Ahmed");
  });

  it("returns null name when both fullName and firstName are null", async () => {
    setClerkConfigured();
    mockCurrentUser.mockResolvedValue({
      ...fakeClerkUser,
      fullName: null,
      firstName: null,
    });

    const result = await getClerkUser();
    expect(result?.name).toBeNull();
  });

  it("returns null email when primary email address is not in emailAddresses list", async () => {
    setClerkConfigured();
    mockCurrentUser.mockResolvedValue({
      ...fakeClerkUser,
      primaryEmailAddressId: "email_does_not_exist",
    });

    const result = await getClerkUser();
    // Still returns user, but email is null
    expect(result).not.toBeNull();
    expect(result?.email).toBeNull();
  });

  it("returns null imageUrl when imageUrl is null", async () => {
    setClerkConfigured();
    mockCurrentUser.mockResolvedValue({
      ...fakeClerkUser,
      imageUrl: null,
    });

    const result = await getClerkUser();
    expect(result?.imageUrl).toBeNull();
  });

  it("returns null when currentUser() throws (error boundary)", async () => {
    setClerkConfigured();
    mockCurrentUser.mockRejectedValue(new Error("Clerk SDK unavailable"));

    const result = await getClerkUser();
    expect(result).toBeNull();
  });
});

// ── getClerkUserId ────────────────────────────────────────────────────────────

describe("getClerkUserId", () => {
  it("returns null when NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is not set", async () => {
    clearClerkConfig();
    const result = await getClerkUserId();
    expect(result).toBeNull();
    expect(mockAuth).not.toHaveBeenCalled();
  });

  it("returns null when auth() has no userId (unauthenticated)", async () => {
    setClerkConfigured();
    mockAuth.mockResolvedValue({ userId: null });

    const result = await getClerkUserId();
    expect(result).toBeNull();
  });

  it("returns userId string from auth()", async () => {
    setClerkConfigured();
    mockAuth.mockResolvedValue({ userId: "user_abc123" });

    const result = await getClerkUserId();
    expect(result).toBe("user_abc123");
  });

  it("returns null when auth() throws (error boundary)", async () => {
    setClerkConfigured();
    mockAuth.mockRejectedValue(new Error("Clerk SDK failure"));

    const result = await getClerkUserId();
    expect(result).toBeNull();
  });
});

// ── requireClerkAuth ──────────────────────────────────────────────────────────

describe("requireClerkAuth", () => {
  it("throws CLERK_UNAUTHENTICATED when auth() returns no userId", async () => {
    mockAuth.mockResolvedValue({ userId: null });

    await expect(requireClerkAuth()).rejects.toThrow("CLERK_UNAUTHENTICATED");
  });

  it("throws CLERK_USER_NOT_FOUND when userId exists but user lookup fails", async () => {
    // auth() returns a userId but currentUser() returns null
    mockAuth.mockResolvedValue({ userId: "user_orphaned" });
    // getClerkUser calls currentUser() — if Clerk not configured it returns null
    // We make Clerk configured so currentUser() null triggers CLERK_USER_NOT_FOUND
    setClerkConfigured();
    mockCurrentUser.mockResolvedValue(null);

    await expect(requireClerkAuth()).rejects.toThrow("CLERK_USER_NOT_FOUND");
  });

  it("returns ClerkAuthUser when fully authenticated", async () => {
    setClerkConfigured();
    mockAuth.mockResolvedValue({ userId: fakeClerkUser.id });
    mockCurrentUser.mockResolvedValue(fakeClerkUser);

    const user = await requireClerkAuth();

    expect(user.id).toBe("user_clerk_123");
    expect(user.email).toBe("ahmed@mawidi.com");
    expect(user.name).toBe("Ahmed Al-Rashid");
  });

  it("the returned user matches ClerkAuthUser interface", async () => {
    setClerkConfigured();
    mockAuth.mockResolvedValue({ userId: fakeClerkUser.id });
    mockCurrentUser.mockResolvedValue(fakeClerkUser);

    const user = await requireClerkAuth();

    // Verify shape (all required fields present)
    expect(typeof user.id).toBe("string");
    expect(user).toHaveProperty("email");
    expect(user).toHaveProperty("name");
    expect(user).toHaveProperty("imageUrl");
  });
});
