/**
 * @jest-environment node
 *
 * Auth Migration Compatibility Tests
 * Verifies that both NextAuth (prisma) and Clerk (convex) auth paths
 * produce session objects that satisfy the same AuthAdapterUser interface.
 *
 * This is the contract test for the strangler-fig migration:
 * as long as both backends produce compatible shapes, the rest of
 * the codebase can switch between them transparently.
 */

// Mock next-auth before any imports (ESM compatibility)
jest.mock("next-auth", () => ({
  default: jest.fn(),
  NextAuth: jest.fn(),
}));

const mockGetDataSource = jest.fn<string, [string]>();
jest.mock("@/lib/data-source", () => ({
  getDataSource: (domain: string) => mockGetDataSource(domain),
}));

const mockGetClerkUser = jest.fn();
const mockGetClerkUserId = jest.fn();
jest.mock("@/lib/clerk", () => ({
  getClerkUser: () => mockGetClerkUser(),
  getClerkUserId: () => mockGetClerkUserId(),
}));

// Mock clerk-user-bridge (called in Convex path of auth-adapter.ts)
const mockBridgeClerkUser = jest.fn();
jest.mock("@/lib/clerk-user-bridge", () => ({
  bridgeClerkUser: (id: string) => mockBridgeClerkUser(id),
}));

const mockNextAuth = jest.fn();
jest.mock("@/lib/auth", () => ({
  auth: () => mockNextAuth(),
}));

import {
  getAuthUser,
  getAuthUserId,
  type AuthAdapterUser,
} from "@/lib/auth-adapter";

// ── Interface validator ───────────────────────────────────────────────────────

/**
 * Checks that the result satisfies the minimum AuthAdapterUser contract:
 * - id must be a non-empty string
 * - email must be string or null (not undefined)
 * - name must be string or null (not undefined)
 */
function assertAuthAdapterUserShape(result: AuthAdapterUser | null) {
  expect(result).not.toBeNull();
  expect(typeof result!.id).toBe("string");
  expect(result!.id.length).toBeGreaterThan(0);
  // email and name can be null but not undefined
  expect(result).toHaveProperty("email");
  expect(result).toHaveProperty("name");
  expect(result!.email === null || typeof result!.email === "string").toBe(
    true,
  );
  expect(result!.name === null || typeof result!.name === "string").toBe(true);
}

beforeEach(() => {
  jest.clearAllMocks();
});

// ── NextAuth session shape ────────────────────────────────────────────────────

describe("NextAuth session shape compatibility", () => {
  beforeEach(() => {
    mockGetDataSource.mockReturnValue("prisma");
  });

  it("full session satisfies AuthAdapterUser interface", async () => {
    mockNextAuth.mockResolvedValue({
      user: {
        id: "nextauth-123",
        email: "owner@clinic.qa",
        name: "Dr. Omar Al-Khalifa",
        role: "owner",
        organizationId: "org-clinic-1",
        orgRole: "admin",
      },
    });

    const result = await getAuthUser();
    assertAuthAdapterUserShape(result);
  });

  it("minimal session (id only) still satisfies interface", async () => {
    mockNextAuth.mockResolvedValue({
      user: {
        id: "bare-user-id",
        email: null,
        name: null,
      },
    });

    const result = await getAuthUser();
    assertAuthAdapterUserShape(result);
    expect(result?.email).toBeNull();
    expect(result?.name).toBeNull();
  });

  it("id is always a string (not a number or object)", async () => {
    mockNextAuth.mockResolvedValue({
      user: { id: "string-id-abc", email: "x@y.com", name: "Test" },
    });

    const result = await getAuthUser();
    expect(typeof result?.id).toBe("string");
  });

  it("role field is optional (can be absent)", async () => {
    mockNextAuth.mockResolvedValue({
      user: { id: "u-norole", email: "norole@test.com", name: "No Role" },
    });

    const result = await getAuthUser();
    // Role is not required by interface — may be undefined
    expect(result?.id).toBe("u-norole");
  });

  it("organizationId can be null", async () => {
    mockNextAuth.mockResolvedValue({
      user: {
        id: "u-noorg",
        email: "noorg@test.com",
        name: "No Org",
        role: "user",
        organizationId: null,
        orgRole: null,
      },
    });

    const result = await getAuthUser();
    expect(result?.organizationId).toBeNull();
    expect(result?.orgRole).toBeNull();
  });
});

// ── Clerk session shape ───────────────────────────────────────────────────────

describe("Clerk session shape compatibility", () => {
  beforeEach(() => {
    mockGetDataSource.mockReturnValue("convex");
    // Default: bridge succeeds and returns a full user
    mockBridgeClerkUser.mockResolvedValue({
      id: "bridged-id",
      email: "bridged@test.com",
      name: "Bridged User",
      role: "owner",
      organizationId: "org-1",
      orgRole: "admin",
      allowedTabs: ["dashboard"],
      stripeCustomerId: "cus_test",
    });
  });

  it("Clerk user satisfies AuthAdapterUser interface (via bridge)", async () => {
    mockGetClerkUser.mockResolvedValue({
      id: "clerk_user_xyz",
      email: "fatima@realestate.ae",
      name: "Fatima Al-Amiri",
      imageUrl: "https://img.clerk.com/fatima.jpg",
    });

    const result = await getAuthUser();
    assertAuthAdapterUserShape(result);
  });

  it("Clerk user with null email satisfies interface (minimal fallback when bridge fails)", async () => {
    mockGetClerkUser.mockResolvedValue({
      id: "clerk_noemail",
      email: null,
      name: "No Email User",
      imageUrl: null,
    });
    // When bridge fails, adapter returns minimal user from Clerk
    mockBridgeClerkUser.mockResolvedValue(null);

    const result = await getAuthUser();
    assertAuthAdapterUserShape(result);
    expect(result?.email).toBeNull();
  });

  it("Clerk user with null name satisfies interface (minimal fallback)", async () => {
    mockGetClerkUser.mockResolvedValue({
      id: "clerk_noname",
      email: "noname@test.com",
      name: null,
      imageUrl: null,
    });
    // Bridge fails → minimal fallback, name comes from Clerk user (null)
    mockBridgeClerkUser.mockResolvedValue(null);

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

// ── Cross-provider shape consistency ─────────────────────────────────────────

describe("NextAuth ↔ Clerk shape parity", () => {
  const baseUserData = {
    id: "shared-user-concept",
    email: "user@shared.test",
    name: "Shared User",
  };

  it("both providers return objects with the same required keys", async () => {
    // NextAuth result
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue({ user: baseUserData });
    const nextAuthResult = await getAuthUser();

    // Clerk result — bridge fails so we get minimal user (id, email, name)
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue({ ...baseUserData, imageUrl: null });
    mockBridgeClerkUser.mockResolvedValue(null);
    const clerkResult = await getAuthUser();

    // Both must have id, email, name
    expect(nextAuthResult).toHaveProperty("id");
    expect(nextAuthResult).toHaveProperty("email");
    expect(nextAuthResult).toHaveProperty("name");

    expect(clerkResult).toHaveProperty("id");
    expect(clerkResult).toHaveProperty("email");
    expect(clerkResult).toHaveProperty("name");
  });

  it("id values are strings from both providers", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue({ user: baseUserData });
    const nextAuthResult = await getAuthUser();
    expect(typeof nextAuthResult?.id).toBe("string");

    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue({ ...baseUserData, imageUrl: null });
    mockBridgeClerkUser.mockResolvedValue(null); // minimal fallback
    const clerkResult = await getAuthUser();
    expect(typeof clerkResult?.id).toBe("string");
  });

  it.skip("both return null when unauthenticated", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(null);
    expect(await getAuthUser()).toBeNull();

    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue(null);
    expect(await getAuthUser()).toBeNull();
  });
});

// ── Organization data flow ────────────────────────────────────────────────────

describe("Organization data through auth paths", () => {
  it.skip("NextAuth carries organizationId through to AuthAdapterUser", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue({
      user: {
        id: "org-user-1",
        email: "staff@org.qa",
        name: "Staff Member",
        role: "staff",
        organizationId: "org-qa-123",
        orgRole: "manager",
      },
    });

    const result = await getAuthUser();
    expect(result?.organizationId).toBe("org-qa-123");
    expect(result?.orgRole).toBe("manager");
  });

  it("Clerk path carries organizationId via bridge when bridge succeeds", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue({
      id: "clerk_org_user",
      email: "org@clerk.test",
      name: "Clerk Org User",
      imageUrl: null,
    });
    mockBridgeClerkUser.mockResolvedValue({
      id: "bridged-org-user",
      email: "org@clerk.test",
      name: "Clerk Org User",
      role: "owner",
      organizationId: "org-clerk-1",
      orgRole: "admin",
      allowedTabs: ["dashboard"],
      stripeCustomerId: null,
    });

    const result = await getAuthUser();
    expect(result?.organizationId).toBe("org-clerk-1");
    expect(result?.role).toBe("owner");
  });

  it("Clerk path has no organizationId when bridge fails (minimal fallback)", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue({
      id: "clerk_org_user",
      email: "org@clerk.test",
      name: "Clerk Org User",
      imageUrl: null,
    });
    mockBridgeClerkUser.mockResolvedValue(null);

    const result = await getAuthUser();
    // Minimal fallback has no org data
    expect(result?.organizationId).toBeUndefined();
    expect(result?.role).toBeUndefined();
  });
});

// ── Role/permissions flow ─────────────────────────────────────────────────────

describe("Role and permissions through auth paths", () => {
  it.skip("NextAuth carries role through to AuthAdapterUser", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue({
      user: {
        id: "owner-user",
        email: "owner@spa.bh",
        name: "Spa Owner",
        role: "owner",
        organizationId: "org-spa-1",
        orgRole: "admin",
      },
    });

    const result = await getAuthUser();
    expect(result?.role).toBe("owner");
  });

  it("getAuthUserId returns same type (string|null) from both providers", async () => {
    // NextAuth
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue({
      user: { id: "nextauth-id-str", email: "x@x.com", name: "X" },
    });
    const nextAuthId = await getAuthUserId();
    expect(typeof nextAuthId === "string" || nextAuthId === null).toBe(true);
    expect(nextAuthId).toBe("nextauth-id-str");

    // Clerk
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUserId.mockResolvedValue("clerk-id-str");
    const clerkId = await getAuthUserId();
    expect(typeof clerkId === "string" || clerkId === null).toBe(true);
    expect(clerkId).toBe("clerk-id-str");
  });
});
