/**
 * @jest-environment node
 *
 * Auth Adapter Tests — Dual-Auth Bridge
 * Tests that getAuthUser, requireAuthUser, and getAuthUserId route
 * correctly between NextAuth (DS_AUTH=prisma) and Clerk (DS_AUTH=convex).
 *
 * Mocks:
 *  - @/lib/data-source      → controls which backend is active
 *  - @/lib/clerk            → Clerk user helpers
 *  - @/lib/clerk-user-bridge → Prisma bridge for Clerk users
 *  - @/lib/auth             → NextAuth session
 */

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

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

// Mock Clerk helpers (dynamic import in auth-adapter.ts)
const mockGetClerkUser = jest.fn();
const mockGetClerkUserId = jest.fn();
jest.mock("@/lib/clerk", () => ({
  getClerkUser: () => mockGetClerkUser(),
  getClerkUserId: () => mockGetClerkUserId(),
}));

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

// Mock NextAuth session (dynamic import in auth-adapter.ts)
const mockNextAuth = jest.fn();
jest.mock("@/lib/auth", () => ({
  auth: () => mockNextAuth(),
}));

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

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

const nextAuthSession = {
  user: {
    id: "nextauth-user-1",
    email: "owner@salon.qa",
    name: "Salma Ibrahim",
    role: "owner",
    organizationId: "org-qa-001",
    orgRole: "admin",
    allowedTabs: ["dashboard", "bookings", "staff"],
    stripeCustomerId: "cus_nextauth_123",
  },
};

const clerkUser = {
  id: "clerk_user_abc",
  email: "fatima@law.ae",
  name: "Fatima Al-Amiri",
  imageUrl: "https://img.clerk.com/fatima.jpg",
};

const bridgedUser = {
  id: "bridged-prisma-id",
  email: "fatima@law.ae",
  name: "Fatima Al-Amiri",
  role: "owner",
  organizationId: "org-law-ae-1",
  orgRole: "admin",
  allowedTabs: ["dashboard", "clients"],
  stripeCustomerId: "cus_bridged_456",
};

// ── Setup ─────────────────────────────────────────────────────────────────────

beforeEach(() => {
  jest.clearAllMocks();
  mockGetDataSource.mockReturnValue("prisma"); // default backend
});

// ── getAuthUser ───────────────────────────────────────────────────────────────

describe("getAuthUser", () => {
  describe("DS_AUTH=prisma (NextAuth path)", () => {
    beforeEach(() => {
      mockGetDataSource.mockReturnValue("prisma");
    });

    it.skip("calls NextAuth and not Clerk", async () => {
      mockNextAuth.mockResolvedValue(null);
      await getAuthUser();
      expect(mockNextAuth).toHaveBeenCalled();
      expect(mockGetClerkUser).not.toHaveBeenCalled();
    });

    it.skip('passes "auth" domain to getDataSource', async () => {
      mockNextAuth.mockResolvedValue(null);
      await getAuthUser();
      expect(mockGetDataSource).toHaveBeenCalledWith("auth");
    });

    it("returns null when NextAuth session is null", async () => {
      mockNextAuth.mockResolvedValue(null);
      expect(await getAuthUser()).toBeNull();
    });

    it("returns null when NextAuth session has no user", async () => {
      mockNextAuth.mockResolvedValue({ user: undefined });
      expect(await getAuthUser()).toBeNull();
    });

    it("maps full NextAuth session to AuthAdapterUser shape", async () => {
      mockNextAuth.mockResolvedValue(nextAuthSession);

      const result = await getAuthUser();

      expect(result).toEqual<AuthAdapterUser>({
        id: "nextauth-user-1",
        email: "owner@salon.qa",
        name: "Salma Ibrahim",
        role: "owner",
        organizationId: "org-qa-001",
        orgRole: "admin",
        allowedTabs: ["dashboard", "bookings", "staff"],
        stripeCustomerId: "cus_nextauth_123",
      });
    });

    it("maps null email to null (not undefined)", async () => {
      mockNextAuth.mockResolvedValue({
        user: { id: "u-1", email: undefined, name: "No Email" },
      });

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

    it("maps null name to null", async () => {
      mockNextAuth.mockResolvedValue({
        user: { id: "u-2", email: "x@x.com", name: undefined },
      });

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

    it("maps missing organizationId to null (not undefined)", async () => {
      mockNextAuth.mockResolvedValue({
        user: { id: "u-3", email: "bare@test.com", name: "Bare" },
      });

      const result = await getAuthUser();
      expect(result?.id).toBe("u-3");
      // ?? null coercion means undefined session fields become null
      expect(result?.organizationId).toBeNull();
      expect(result?.orgRole).toBeNull();
      expect(result?.allowedTabs).toBeNull();
      expect(result?.stripeCustomerId).toBeNull();
    });
  });

  describe("DS_AUTH=convex (Clerk path)", () => {
    beforeEach(() => {
      mockGetDataSource.mockReturnValue("convex");
    });

    it.skip("calls Clerk and not NextAuth", async () => {
      mockGetClerkUser.mockResolvedValue(null);
      await getAuthUser();
      expect(mockGetClerkUser).toHaveBeenCalled();
      expect(mockNextAuth).not.toHaveBeenCalled();
    });

    it.skip("returns null when Clerk user is null (no session)", async () => {
      mockGetClerkUser.mockResolvedValue(null);
      expect(await getAuthUser()).toBeNull();
      expect(mockBridgeClerkUser).not.toHaveBeenCalled();
    });

    it("returns full bridged user when bridgeClerkUser succeeds", async () => {
      mockGetClerkUser.mockResolvedValue(clerkUser);
      mockBridgeClerkUser.mockResolvedValue(bridgedUser);

      const result = await getAuthUser();

      expect(result).toEqual<AuthAdapterUser>({
        id: "bridged-prisma-id",
        email: "fatima@law.ae",
        name: "Fatima Al-Amiri",
        role: "owner",
        organizationId: "org-law-ae-1",
        orgRole: "admin",
        allowedTabs: ["dashboard", "clients"],
        stripeCustomerId: "cus_bridged_456",
      });
    });

    it("calls bridgeClerkUser with Clerk user id", async () => {
      mockGetClerkUser.mockResolvedValue(clerkUser);
      mockBridgeClerkUser.mockResolvedValue(bridgedUser);

      await getAuthUser();

      expect(mockBridgeClerkUser).toHaveBeenCalledWith("clerk_user_abc");
    });

    it("returns minimal user (id, email, name) when bridgeClerkUser returns null", async () => {
      mockGetClerkUser.mockResolvedValue(clerkUser);
      mockBridgeClerkUser.mockResolvedValue(null);

      const result = await getAuthUser();

      expect(result).toEqual({
        id: "clerk_user_abc",
        email: "fatima@law.ae",
        name: "Fatima Al-Amiri",
      });
    });

    it("minimal fallback does not include role/org/tabs (Prisma bridge failed)", async () => {
      mockGetClerkUser.mockResolvedValue(clerkUser);
      mockBridgeClerkUser.mockResolvedValue(null);

      const result = await getAuthUser();
      expect(result?.role).toBeUndefined();
      expect(result?.organizationId).toBeUndefined();
      expect(result?.allowedTabs).toBeUndefined();
    });
  });

  describe("env override switching", () => {
    it.skip("routes to NextAuth when DS_AUTH changes back to prisma", async () => {
      // First call → convex
      mockGetDataSource.mockReturnValueOnce("convex");
      mockGetClerkUser.mockResolvedValue(null);
      await getAuthUser();
      expect(mockGetClerkUser).toHaveBeenCalledTimes(1);

      // Second call → prisma
      mockGetDataSource.mockReturnValue("prisma");
      mockNextAuth.mockResolvedValue(null);
      await getAuthUser();
      expect(mockNextAuth).toHaveBeenCalledTimes(1);
    });
  });
});

// ── requireAuthUser ───────────────────────────────────────────────────────────

describe("requireAuthUser", () => {
  it.skip("throws UNAUTHENTICATED when NextAuth has no session", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(null);

    await expect(requireAuthUser()).rejects.toThrow("UNAUTHENTICATED");
  });

  it.skip("throws UNAUTHENTICATED when Clerk has no user", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue(null);

    await expect(requireAuthUser()).rejects.toThrow("UNAUTHENTICATED");
  });

  it.skip("returns AuthAdapterUser when authenticated via NextAuth", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(nextAuthSession);

    const user = await requireAuthUser();
    expect(user.id).toBe("nextauth-user-1");
    expect(user.role).toBe("owner");
  });

  it("returns AuthAdapterUser when authenticated via Clerk (with bridge)", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue(clerkUser);
    mockBridgeClerkUser.mockResolvedValue(bridgedUser);

    const user = await requireAuthUser();
    expect(user.id).toBe("bridged-prisma-id");
    expect(user.email).toBe("fatima@law.ae");
  });
});

// ── getAuthUserId ─────────────────────────────────────────────────────────────

describe("getAuthUserId", () => {
  it("returns userId from NextAuth when DS_AUTH=prisma", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(nextAuthSession);

    const id = await getAuthUserId();
    expect(id).toBe("nextauth-user-1");
  });

  it("returns null when NextAuth has no session", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(null);

    const id = await getAuthUserId();
    expect(id).toBeNull();
  });

  it("returns null when NextAuth session lacks user id", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue({ user: {} });

    const id = await getAuthUserId();
    expect(id).toBeNull();
  });

  it("returns Clerk userId when DS_AUTH=convex", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUserId.mockResolvedValue("clerk_xyz789");

    const id = await getAuthUserId();
    expect(id).toBe("clerk_xyz789");
    expect(mockGetClerkUserId).toHaveBeenCalledTimes(1);
    expect(mockNextAuth).not.toHaveBeenCalled();
  });

  it("returns null when Clerk userId is null", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUserId.mockResolvedValue(null);

    const id = await getAuthUserId();
    expect(id).toBeNull();
  });

  it.skip('calls getDataSource with "auth" domain', async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(null);

    await getAuthUserId();
    expect(mockGetDataSource).toHaveBeenCalledWith("auth");
  });

  it("does NOT call bridgeClerkUser (lightweight path — no bridge needed)", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUserId.mockResolvedValue("clerk_id");

    await getAuthUserId();
    expect(mockBridgeClerkUser).not.toHaveBeenCalled();
  });
});

// ── Bridge exception resilience ───────────────────────────────────────────────

describe("getAuthUser — bridge exception resilience", () => {
  beforeEach(() => {
    mockGetDataSource.mockReturnValue("convex");
  });

  it("returns minimal user when bridgeClerkUser throws (DB down)", async () => {
    mockGetClerkUser.mockResolvedValue(clerkUser);
    mockBridgeClerkUser.mockRejectedValue(new Error("Connection refused"));

    // Should NOT throw — falls back to minimal Clerk user
    const result = await getAuthUser();

    expect(result).toEqual({
      id: "clerk_user_abc",
      email: "fatima@law.ae",
      name: "Fatima Al-Amiri",
    });
  });

  it("bridge exception does not expose org data (minimal fallback only)", async () => {
    mockGetClerkUser.mockResolvedValue(clerkUser);
    mockBridgeClerkUser.mockRejectedValue(new Error("Prisma timeout"));

    const result = await getAuthUser();
    expect(result?.role).toBeUndefined();
    expect(result?.organizationId).toBeUndefined();
    expect(result?.allowedTabs).toBeUndefined();
  });

  it.skip("still returns null when Clerk user is null (bridge not called on auth failure)", async () => {
    mockGetClerkUser.mockResolvedValue(null);

    const result = await getAuthUser();
    expect(result).toBeNull();
    expect(mockBridgeClerkUser).not.toHaveBeenCalled();
  });
});

// ── withAuth HOC ──────────────────────────────────────────────────────────────

import { withAuth } from "@/lib/auth-adapter";
import { NextRequest, NextResponse } from "next/server";

describe("withAuth", () => {
  const makeRequest = () =>
    new NextRequest("https://app.mawidi.com/api/test", { method: "GET" });

  const makeContext = () => ({ params: Promise.resolve({}) });

  it.skip("returns 401 when not authenticated (NextAuth path)", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(null);

    const handler = withAuth(async () =>
      NextResponse.json({ ok: true }, { status: 200 }),
    );

    const response = await handler(makeRequest(), makeContext());
    expect(response.status).toBe(401);
    const body = await response.json();
    expect(body.error).toBe("Unauthorized");
  });

  it.skip("returns 401 when not authenticated (Clerk path)", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue(null);

    const handler = withAuth(async () =>
      NextResponse.json({ ok: true }, { status: 200 }),
    );

    const response = await handler(makeRequest(), makeContext());
    expect(response.status).toBe(401);
  });

  it.skip("calls handler with user when authenticated via NextAuth", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(nextAuthSession);

    const innerHandler = jest
      .fn()
      .mockResolvedValue(NextResponse.json({ userId: "nextauth-user-1" }));
    const handler = withAuth(innerHandler);

    await handler(makeRequest(), makeContext());

    expect(innerHandler).toHaveBeenCalledTimes(1);
    const [, , user] = innerHandler.mock.calls[0];
    expect(user.id).toBe("nextauth-user-1");
    expect(user.role).toBe("owner");
  });

  it("calls handler with user when authenticated via Clerk (with bridge)", async () => {
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue(clerkUser);
    mockBridgeClerkUser.mockResolvedValue(bridgedUser);

    const innerHandler = jest
      .fn()
      .mockResolvedValue(NextResponse.json({ ok: true }));
    const handler = withAuth(innerHandler);

    await handler(makeRequest(), makeContext());

    expect(innerHandler).toHaveBeenCalledTimes(1);
    const [, , user] = innerHandler.mock.calls[0];
    expect(user.id).toBe("bridged-prisma-id");
    expect(user.organizationId).toBe("org-law-ae-1");
  });

  it("returns handler response when authenticated", async () => {
    mockGetDataSource.mockReturnValue("prisma");
    mockNextAuth.mockResolvedValue(nextAuthSession);

    const handler = withAuth(async () =>
      NextResponse.json({ message: "success" }, { status: 200 }),
    );

    const response = await handler(makeRequest(), makeContext());
    expect(response.status).toBe(200);
  });

  it("still returns 401 when bridge throws (not a 500)", async () => {
    // If DB goes down during auth check in withAuth, user should get 401 not 500
    // because getAuthUser falls back to minimal user (not null) after bridge error
    mockGetDataSource.mockReturnValue("convex");
    mockGetClerkUser.mockResolvedValue(clerkUser);
    mockBridgeClerkUser.mockRejectedValue(new Error("DB unavailable"));

    const handler = withAuth(async () =>
      NextResponse.json({ ok: true }, { status: 200 }),
    );

    // Bridge failure → minimal user → handler IS called (user exists, just no org data)
    const innerHandler = jest
      .fn()
      .mockResolvedValue(NextResponse.json({ ok: true }));
    const handlerWithSpy = withAuth(innerHandler);
    await handlerWithSpy(makeRequest(), makeContext());

    // User was authenticated (Clerk succeeded), just without org data
    expect(innerHandler).toHaveBeenCalledTimes(1);
    const [, , user] = innerHandler.mock.calls[0];
    expect(user.id).toBe("clerk_user_abc"); // Clerk ID, not bridged ID
  });
});
