// src/lib/billing/seranking-fulfillment.test.ts — SE-Ranking sync pack (slice 2).
// Mirror of pack.test.ts's fulfillment + checkout-guard suite. DB-gated:
// DB_TESTS=1 DATABASE_URL=postgresql://humperdoo@localhost:5432/seo_insight_test \
//   bun test src/lib/billing/seranking-fulfillment.test.ts
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
import type Stripe from "stripe";

mock.module("server-only", () => ({}));
// fulfillSerankingPack → creditSyncs (balance.ts) doesn't touch next/cache
// directly, but balance.ts imports entitlements.ts's isAddonEnabled, which is
// unstable_cache-wrapped — stub it the same as the other billing/mcp webhook
// test suites so a leaked mock.module from a sibling test file (bun mock.module
// registrations are process-global) can't shadow this with an incompatible one.
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));

const RUN_DB = process.env.DB_TESTS === "1";

/** Minimal fake checkout.session.completed event (cast — we only read the
 *  fields fulfillment uses). */
function fakeEvent(over: {
  type?: string;
  id?: string;
  mode?: string;
  metadata?: Record<string, string> | null;
  amountTotal?: number | null;
}): Stripe.Event {
  return {
    type: over.type ?? "checkout.session.completed",
    data: {
      object: {
        id: over.id ?? "cs_test_srk_pack_1",
        mode: over.mode ?? "payment",
        metadata: over.metadata === undefined ? {} : over.metadata,
        amount_total: over.amountTotal ?? 999,
      },
    },
  } as unknown as Stripe.Event;
}

describe.skipIf(!RUN_DB)("seranking pack fulfillment (postgres)", () => {
  let db: typeof import("@/lib/db/client").db;
  let authSchema: typeof import("@/lib/db/auth-schema");
  let schema: typeof import("@/lib/db/domain-schema");

  // Distinct ids from the MCP pack suite (pack.test.ts) and from the parallel
  // seranking client/sync/actions work — never "test-*" or "org-*" prefixes
  // those own.
  const ORG = "org-srkbill-fulfill-1";

  async function cleanup() {
    const { eq } = await import("drizzle-orm");
    await db
      .delete(schema.serankingSyncPurchases)
      .where(eq(schema.serankingSyncPurchases.orgId, ORG));
    await db
      .delete(authSchema.organization)
      .where(eq(authSchema.organization.id, ORG));
  }

  async function getBalance(): Promise<number> {
    const { eq } = await import("drizzle-orm");
    const [row] = await db
      .select({ balance: authSchema.organization.serankingSyncBalance })
      .from(authSchema.organization)
      .where(eq(authSchema.organization.id, ORG))
      .limit(1);
    return row?.balance ?? -999;
  }

  beforeAll(async () => {
    ({ db } = await import("@/lib/db/client"));
    authSchema = await import("@/lib/db/auth-schema");
    schema = await import("@/lib/db/domain-schema");
    const { assertTestDb } = await import("@/lib/db/assert-test-db");
    await assertTestDb();
    await cleanup();
    const now = new Date();
    await db.insert(authSchema.organization).values({
      id: ORG,
      name: "Test SE-Ranking Billing Org",
      slug: `org-srkbill-fulfill-1-${crypto.randomUUID().slice(0, 8)}`,
      createdAt: now,
    });
  });

  afterAll(async () => {
    await cleanup();
  });

  test("same checkout session twice credits the balance ONCE", async () => {
    const { fulfillSerankingPack } = await import("./seranking-fulfillment");
    const event = fakeEvent({
      id: "cs_test_srk_idem",
      metadata: { type: "seranking_pack", orgId: ORG, syncs: "100" },
    });

    await fulfillSerankingPack(event);
    expect(await getBalance()).toBe(100);

    // Webhook retry — UNIQUE(stripe_session_id) makes it a no-op.
    await fulfillSerankingPack(event);
    expect(await getBalance()).toBe(100);

    const { eq } = await import("drizzle-orm");
    const rows = await db
      .select({ id: schema.serankingSyncPurchases.id })
      .from(schema.serankingSyncPurchases)
      .where(eq(schema.serankingSyncPurchases.orgId, ORG));
    expect(rows.length).toBe(1);
  });

  test("non seranking-pack events are ignored", async () => {
    const { fulfillSerankingPack } = await import("./seranking-fulfillment");
    const before = await getBalance();

    // Wrong event type.
    await fulfillSerankingPack(
      fakeEvent({ type: "invoice.paid", id: "cs_srk_other_1" }),
    );
    // Subscription checkout (the BYOK add-on), not a one-off payment.
    await fulfillSerankingPack(
      fakeEvent({
        id: "cs_srk_other_2",
        mode: "subscription",
        metadata: { type: "seranking_pack", orgId: ORG, syncs: "100" },
      }),
    );
    // Payment checkout without the seranking_pack metadata.
    await fulfillSerankingPack(
      fakeEvent({ id: "cs_srk_other_3", metadata: {} }),
    );
    // Pack metadata but no orgId.
    await fulfillSerankingPack(
      fakeEvent({ id: "cs_srk_other_4", metadata: { type: "seranking_pack" } }),
    );
    // The OTHER pack's type (mcp_pack) — must not credit the seranking balance.
    await fulfillSerankingPack(
      fakeEvent({
        id: "cs_srk_other_5",
        metadata: { type: "mcp_pack", orgId: ORG, credits: "25000" },
      }),
    );

    expect(await getBalance()).toBe(before);
  });

  test("negative syncs metadata is clamped to the floor (1), never negative", async () => {
    const { fulfillSerankingPack } = await import("./seranking-fulfillment");
    const before = await getBalance();
    await fulfillSerankingPack(
      fakeEvent({
        id: "cs_srk_clamp_negative",
        metadata: { type: "seranking_pack", orgId: ORG, syncs: "-100" },
      }),
    );
    expect(await getBalance()).toBe(before + 1);
  });

  test("fractional syncs metadata is floored to an integer", async () => {
    const { fulfillSerankingPack } = await import("./seranking-fulfillment");
    const before = await getBalance();
    await fulfillSerankingPack(
      fakeEvent({
        id: "cs_srk_clamp_fractional",
        metadata: { type: "seranking_pack", orgId: ORG, syncs: "250.7" },
      }),
    );
    expect(await getBalance()).toBe(before + 250);
  });
});
