/**
 * W7 Integration test: install → browse products → admin dashboard → purchase flow
 *
 * CI-verifiable: embedded-pg, no Stripe keys, no CF bindings.
 * Exercises the core data paths that W5 (admin) and W6 (public) screens query,
 * plus the startCheckout → settled provider → getCheckoutStatus end-to-end path.
 */
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { sql } from 'drizzle-orm';
import { listProducts, product, variant, variantPrice, type CatalogSchema } from '@platform-modules/commerce-catalog';
import { listOrders, createOrder, type OrdersSchema, type Actor } from '@platform-modules/commerce-orders';
import type { Querier, Transaction, TransactionalDatabase } from '@platform-modules/db';
import { appendEntry } from '@platform-modules/ledger';
import type { LedgerSeam } from '@platform-modules/billing';
import {
  startCheckout,
  getCheckoutStatus,
  isOrderNotFoundError,
  pushSchema as pushCheckoutSchema,
  type CheckoutDeps,
  type CheckoutSchema,
} from '@platform-modules/commerce-checkout';
import {
  createFakePaymentProvider,
  createFakeIntentStore,
  fakeFulfillmentPorts,
  countAccessGrants,
} from '@platform-modules/commerce-checkout/testing';
import {
  pushSchema as pushFulfillmentSchema,
  type FulfillmentDbSchema,
} from '@platform-modules/commerce-fulfillment';
import { startPg, type StorefrontSchema } from './pg-harness.js';
import { claimInstall, claimInstallOnce, getStoreSettings, isInstalled } from './settings.js';
import { createDbDedupStore, createDbIntentStore } from './checkout-store.js';
import { createNoOpStorageAdapter, createStorefrontFulfillmentPorts } from './fulfillment.js';

const routeHarness = vi.hoisted(() => ({
  cfEnv: {} as Record<string, unknown>,
  session: null as { userId: string; role: string; email: string } | null,
  dbRef: { current: null as TransactionalDatabase<StorefrontSchema> | null },
}));

vi.mock('cloudflare:workers', () => ({
  get env() {
    return routeHarness.cfEnv;
  },
}));

vi.mock('./session.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./session.js')>();
  return {
    ...actual,
    getSession: vi.fn(async () => routeHarness.session),
  };
});

vi.mock('./db.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./db.js')>();
  return {
    ...actual,
    getDb: () => {
      if (!routeHarness.dbRef.current) throw new Error('route test db not ready');
      return { db: routeHarness.dbRef.current, dialect: 'postgres' as const };
    },
    getTransactionalDb: () => {
      if (!routeHarness.dbRef.current) throw new Error('route test db not ready');
      return { db: routeHarness.dbRef.current, dialect: 'postgres' as const };
    },
  };
});

// Defined inline — importing admin.ts pulls in cloudflare:workers which vitest can't resolve.
const ADMIN_ACTOR: Actor = { isAdmin: true };

// StorefrontSchema is a superset of each sub-schema — casts are safe at runtime.
const cast = <T>(v: unknown): T => v as T;

function normalizeRows(result: unknown): unknown[] {
  if (Array.isArray(result)) return result;
  const rows = (result as { rows?: unknown[] } | null)?.rows;
  return rows ?? [];
}

let db: TransactionalDatabase<StorefrontSchema>;
let stopPg: () => Promise<void>;

beforeAll(async () => {
  const harness = await startPg();
  db = harness.db;
  stopPg = harness.stop;
  routeHarness.dbRef.current = db;
}, 120_000);

afterAll(async () => {
  await stopPg?.();
});

describe('install flow', () => {
  it('starts uninstalled', async () => {
    expect(await isInstalled(cast<Querier>(db))).toBe(false);
  });

  it('claimInstallOnce wins on empty table', async () => {
    const won = await claimInstallOnce(cast<Querier>(db));
    expect(won).toBe(true);
  });

  it('claimInstallOnce blocks concurrent (live pending row)', async () => {
    // Row from previous test: value='pending', claimed_at=now() — should block a second attempt.
    const won = await claimInstallOnce(cast<Querier>(db));
    expect(won).toBe(false);
  });

  it('claimInstallOnce reclaims a pending row with NULL claimed_at (pins the OR-IS-NULL fix)', async () => {
    // Simulate ALTER TABLE backfill: existing 'pending' row gets claimed_at=NULL.
    // Without `OR claimed_at IS NULL`, `col < now() - INTERVAL '5 minutes'` evaluates to
    // unknown in Postgres for a NULL col, so the row can never be reclaimed — permanent brick.
    await db.execute(
      sql`UPDATE mod_storefront_settings SET claimed_at = NULL WHERE id = 'installed'`,
    );
    const won = await claimInstallOnce(cast<Querier>(db));
    expect(won).toBe(true);
  });

  it('claimInstall sets installed=true + persists settings', async () => {
    await claimInstall(cast<Querier>(db), {
      storeName: 'Test Store',
      currency: 'USD',
      locale: 'en',
      tagline: 'Best deals',
      themeMode: 'light',
    });

    expect(await isInstalled(cast<Querier>(db))).toBe(true);

    const settings = await getStoreSettings(cast<Querier>(db));
    expect(settings.storeName).toBe('Test Store');
    expect(settings.currency).toBe('USD');
    expect(settings.locale).toBe('en');
    expect(settings.tagline).toBe('Best deals');
    expect(settings.themeMode).toBe('light');
  });
});

describe('browse products', () => {
  const PRODUCT_ID = crypto.randomUUID();
  const VARIANT_ID = crypto.randomUUID();

  it('seeds a product and listProducts returns it', async () => {
    await db.insert(product).values({
      id: PRODUCT_ID,
      kind: 'digital',
      slug: 'test-product',
      title: 'Test Product',
      status: 'active',
      media: [],
      tags: [],
    });
    await db.insert(variant).values({
      id: VARIANT_ID,
      productId: PRODUCT_ID,
      sku: 'TEST-SKU-001',
      attributes: {},
    });
    await db.insert(variantPrice).values({
      variantId: VARIANT_ID,
      currency: 'USD',
      amount: 1999n,
      priceMode: 'exclusive',
    });

    const page = await listProducts(cast<Querier<CatalogSchema>>(db), { audience: 'public', page: 1 });
    expect(page.items.length).toBeGreaterThanOrEqual(1);
    const found = page.items.find((p) => p.id === PRODUCT_ID);
    expect(found).toBeDefined();
    expect(found?.title).toBe('Test Product');
  });

  it('draft products hidden from public audience', async () => {
    const draftId = crypto.randomUUID();
    const draftVid = crypto.randomUUID();
    await db.insert(product).values({
      id: draftId,
      kind: 'digital',
      slug: 'draft-product',
      title: 'Draft Product',
      status: 'draft',
      media: [],
      tags: [],
    });
    await db.insert(variant).values({
      id: draftVid,
      productId: draftId,
      sku: 'DRAFT-SKU',
      attributes: {},
    });

    const publicPage = await listProducts(cast<Querier<CatalogSchema>>(db), { audience: 'public', page: 1 });
    expect(publicPage.items.find((p) => p.id === draftId)).toBeUndefined();

    const adminPage = await listProducts(cast<Querier<CatalogSchema>>(db), { audience: 'admin', page: 1 });
    expect(adminPage.items.find((p) => p.id === draftId)).toBeDefined();
  });
});

describe('admin dashboard data paths', () => {
  it('listOrders with admin actor returns orders', async () => {
    await db.transaction(async (tx) => {
      await createOrder(cast<Transaction<OrdersSchema>>(tx), {
        idempotencyKey: crypto.randomUUID(),
        buyerRef: { userId: crypto.randomUUID() },
        currency: 'USD',
        priceMode: 'exclusive',
        subtotal: 0n,
        tax: 0n,
        discount: 0n,
        total: 0n,
        lines: [],
        splits: [],
      });
    });

    const page = await listOrders(cast<Querier<OrdersSchema>>(db), ADMIN_ACTOR, { limit: 10 });
    expect(page.items.length).toBeGreaterThanOrEqual(1);
  });

  it('listOrders status filter works', async () => {
    const pendingPage = await listOrders(cast<Querier<OrdersSchema>>(db), ADMIN_ACTOR, { status: 'pending', limit: 10 });
    expect(pendingPage.items.every((o) => o.status === 'pending')).toBe(true);
  });
});

describe('purchase flow', () => {
  const PF_PRODUCT_ID = crypto.randomUUID();
  const PF_VARIANT_ID = crypto.randomUUID();

  beforeAll(async () => {
    // Push ledger tables needed by settleCheckout/appendEntry
    await db.execute(sql`
      CREATE TABLE IF NOT EXISTS ledger_entries (
        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
        delta bigint NOT NULL,
        currency text,
        reason text NOT NULL,
        ref jsonb,
        idempotency_key text NOT NULL,
        created_at timestamptz NOT NULL DEFAULT NOW()
      )
    `);
    await db.execute(sql`CREATE UNIQUE INDEX IF NOT EXISTS ledger_entries_idempotency_key_uq ON ledger_entries (idempotency_key)`);
    await db.execute(sql`
      CREATE TABLE IF NOT EXISTS wallet_balances (
        owner_id text PRIMARY KEY,
        balance bigint NOT NULL DEFAULT 0,
        updated_at timestamptz NOT NULL DEFAULT NOW()
      )
    `);
    // Push fulfillment schema (access_grant table) and checkout schema (checkout_session table)
    await pushFulfillmentSchema(cast<Querier<FulfillmentDbSchema>>(db));
    await pushCheckoutSchema(cast<Querier<CheckoutSchema>>(db));

    // Seed a product with variant + price for purchase flow tests.
    // attributes.blobKey is required by the production resolveBlobKey implementation.
    await db.insert(product).values({
      id: PF_PRODUCT_ID,
      kind: 'digital',
      slug: 'pf-widget',
      title: 'PF Widget',
      status: 'active',
      media: [],
      tags: [],
    });
    await db.insert(variant).values({
      id: PF_VARIANT_ID,
      productId: PF_PRODUCT_ID,
      sku: 'PF-WIDGET-001',
      attributes: { blobKey: 'test-assets/pf-widget-001.zip' },
    });
    await db.insert(variantPrice).values({
      variantId: PF_VARIANT_ID,
      currency: 'EUR',
      amount: 999n,
      priceMode: 'exclusive',
    });
  }, 30_000);

  it('full purchase path: real SQL intent+fulfillment, access_grant + settlement ledger entry created', async () => {
    const fakeProvider = createFakePaymentProvider({ kind: 'settled', providerRef: 'pi_test_settled' });
    // Real SQL IntentStore — exercises claimIntent INSERT + ON CONFLICT logic in mod_storefront_charge_intents.
    const intentStore = createDbIntentStore(cast<Querier>(db));
    const ledger: LedgerSeam = { appendEntry: appendEntry as LedgerSeam['appendEntry'] };
    // Real FulfillmentPorts — resolveBlobKey SELECTs variant.attributes.blobKey seeded in beforeAll.
    // createNoOpStorageAdapter throws if put/signedUrl is called; fulfillOrder doesn't touch storage
    // during grant creation (reads blobKey → inserts access_grant row only), so this acts as a guard.
    const fulfillment = createStorefrontFulfillmentPorts(
      cast<TransactionalDatabase<FulfillmentDbSchema>>(db),
      createNoOpStorageAdapter(),
    );

    const deps = {
      db,
      provider: fakeProvider,
      intentStore,
      ledger,
      fulfillment,
    } as unknown as CheckoutDeps<OrdersSchema>;

    const result = await startCheckout(deps, {
      idempotencyKey: crypto.randomUUID(),
      buyerRef: { userId: crypto.randomUUID() },
      priceMode: 'exclusive',
      currency: 'EUR',
      buyerCountry: 'DE',
      cart: {
        id: crypto.randomUUID(),
        currency: 'EUR',
        subtotal: 999n,
        lines: [
          {
            lineId: crypto.randomUUID(),
            variantId: PF_VARIANT_ID,
            qty: 1,
            price: { amount: 999n, currency: 'EUR', priceMode: 'exclusive' },
            vendorId: null,
          },
        ],
      },
    });

    expect(result.orderId).toEqual(expect.any(String));
    // Settled provider → fulfillOrder ran → access_grant row created
    const grantCount = await countAccessGrants(cast<Querier<FulfillmentDbSchema>>(db), result.orderId);
    expect(grantCount).toBeGreaterThanOrEqual(1);
    // Settled provider → appendEntry wrote a settlement:chargeKey row the admin revenue query depends on
    const ledgerRows = normalizeRows(
      await db.execute(sql`SELECT reason FROM ledger_entries WHERE reason LIKE 'settlement:%' LIMIT 10`),
    );
    expect(ledgerRows.length).toBeGreaterThanOrEqual(1);
  });

  it('getCheckoutStatus returns status for a known order', async () => {
    const fakeProvider = createFakePaymentProvider({ kind: 'settled', providerRef: 'pi_test_status' });
    const fakeIntentStore = createFakeIntentStore();
    const ledger: LedgerSeam = { appendEntry: appendEntry as LedgerSeam['appendEntry'] };
    const fakeFulfill = fakeFulfillmentPorts({ db: cast<TransactionalDatabase<FulfillmentDbSchema>>(db) });

    const deps = {
      db,
      provider: fakeProvider,
      intentStore: fakeIntentStore,
      ledger,
      fulfillment: fakeFulfill,
    } as unknown as CheckoutDeps<OrdersSchema>;

    const buyerRef = { userId: crypto.randomUUID() };
    const { orderId } = await startCheckout(deps, {
      idempotencyKey: crypto.randomUUID(),
      buyerRef,
      priceMode: 'exclusive',
      currency: 'EUR',
      buyerCountry: 'DE',
      cart: {
        id: crypto.randomUUID(),
        currency: 'EUR',
        subtotal: 999n,
        lines: [
          {
            lineId: crypto.randomUUID(),
            variantId: PF_VARIANT_ID,
            qty: 1,
            price: { amount: 999n, currency: 'EUR', priceMode: 'exclusive' },
            vendorId: null,
          },
        ],
      },
    });

    const status = await getCheckoutStatus(deps, orderId, buyerRef);
    // Settled provider → order transitions to 'paid' inline
    expect(['paid', 'charging']).toContain(status.status);
  });

  it('getCheckoutStatus throws for unknown orderId', async () => {
    const deps = { db } as unknown as CheckoutDeps<OrdersSchema>;
    await expect(
      getCheckoutStatus(deps, crypto.randomUUID(), { userId: crypto.randomUUID() }),
    ).rejects.toSatisfy(isOrderNotFoundError);
  });
});

describe('dedup store', () => {
  const dedupDb = () => createDbDedupStore(cast<Querier>(db));

  it('first claim wins', async () => {
    const eventId = `evt_${crypto.randomUUID()}`;
    expect(await dedupDb().claim(eventId)).toBe('won');
  });

  it('immediate re-claim within 5 min is lost', async () => {
    const eventId = `evt_${crypto.randomUUID()}`;
    const store = dedupDb();
    await store.claim(eventId);
    expect(await store.claim(eventId)).toBe('lost');
  });

  it('re-claim after 5+ min wins again (simulated by backdating claimed_at)', async () => {
    const eventId = `evt_${crypto.randomUUID()}`;
    const store = dedupDb();
    await store.claim(eventId);
    // Backdate claimed_at to simulate Stripe retry well after the 5-min dedup window.
    await db.execute(sql`
      UPDATE mod_storefront_webhook_events
      SET claimed_at = NOW() - INTERVAL '6 minutes'
      WHERE event_id = ${eventId}
    `);
    expect(await store.claim(eventId)).toBe('won');
  });

  it('already-processed event is never re-claimed even after the window', async () => {
    const eventId = `evt_${crypto.randomUUID()}`;
    const store = dedupDb();
    await store.claim(eventId);
    await store.markProcessed(eventId);
    // Backdate past 5 min: discriminates that processed_at IS NOT NULL — not the time window — is what blocks re-claim.
    await db.execute(sql`
      UPDATE mod_storefront_webhook_events
      SET claimed_at = NOW() - INTERVAL '6 minutes'
      WHERE event_id = ${eventId}
    `);
    expect(await store.claim(eventId)).toBe('lost');
  });
});

describe('security boundaries', () => {
  const SITE = 'https://store.example';
  const TEST_SESSION = { userId: 'sec-user-1', role: 'buyer', email: 'buyer@example.com' };

  let startCheckoutPost: (typeof import('../pages/api/checkout/start.js'))['POST'];
  let webhookPost: (typeof import('../pages/api/checkout/webhook.js'))['POST'];
  let downloadGet: (typeof import('../pages/api/download/[key].js'))['GET'];

  beforeAll(async () => {
    vi.spyOn(Response, 'redirect').mockImplementation((url, status = 302) => {
      return new Response(null, { status, headers: { Location: String(url) } });
    });

    const startMod = await import('../pages/api/checkout/start.js');
    const webhookMod = await import('../pages/api/checkout/webhook.js');
    const downloadMod = await import('../pages/api/download/[key].js');
    startCheckoutPost = startMod.POST;
    webhookPost = webhookMod.POST;
    downloadGet = downloadMod.GET;
    await pushFulfillmentSchema(cast<Querier<FulfillmentDbSchema>>(db));
  });

  function checkoutStartCart(qty: number) {
    return {
      idempotencyKey: crypto.randomUUID(),
      buyerCountry: 'DE',
      currency: 'USD',
      cart: {
        id: crypto.randomUUID(),
        currency: 'USD',
        subtotal: 100,
        lines: [
          {
            lineId: crypto.randomUUID(),
            variantId: crypto.randomUUID(),
            qty,
            price: { amount: 100, currency: 'USD', priceMode: 'exclusive' },
            vendorId: null,
          },
        ],
      },
    };
  }

  function postCheckoutStart(body: Record<string, unknown>) {
    routeHarness.cfEnv = {
      SESSION: {},
      STRIPE_SECRET_KEY: 'sk_test_fake',
      DATABASE_URL: 'postgres://test@localhost/test',
    };
    routeHarness.session = TEST_SESSION;
    return startCheckoutPost({
      request: new Request(`${SITE}/api/checkout/start`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      }),
      cookies: { get: () => undefined, set: vi.fn(), delete: vi.fn() },
    } as never);
  }

  it('checkout start rejects qty 0 with invalid_qty', async () => {
    const res = await postCheckoutStart(checkoutStartCart(0));
    expect(res.status).toBe(422);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe('invalid_qty');
  });

  it('checkout start rejects negative qty with invalid_qty', async () => {
    const res = await postCheckoutStart(checkoutStartCart(-1));
    expect(res.status).toBe(422);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe('invalid_qty');
  });

  it('checkout start rejects non-integer qty with invalid_qty', async () => {
    const res = await postCheckoutStart(checkoutStartCart(1.5));
    expect(res.status).toBe(422);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe('invalid_qty');
  });

  it('checkout start rejects bodies larger than 64KB with 413', async () => {
    const pad = 'x'.repeat(70 * 1024);
    const res = await postCheckoutStart({ ...checkoutStartCart(1), pad });
    expect(res.status).toBe(413);
  });

  it('checkout webhook rejects chunked bodies larger than 512KB with 413', async () => {
    routeHarness.cfEnv = {
      STRIPE_SECRET_KEY: 'sk_test_fake',
      STRIPE_WEBHOOK_SECRET: 'whsec_test_fake',
      DATABASE_URL: 'postgres://test@localhost/test',
    };

    const chunkSize = 64 * 1024;
    const totalBytes = 520 * 1024;
    let sent = 0;
    const body = new ReadableStream<Uint8Array>({
      pull(controller) {
        if (sent >= totalBytes) {
          controller.close();
          return;
        }
        const size = Math.min(chunkSize, totalBytes - sent);
        controller.enqueue(new Uint8Array(size).fill(0x61));
        sent += size;
      },
    });

    const res = await webhookPost({
      request: new Request(`${SITE}/api/checkout/webhook`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body,
        duplex: 'half',
      } as RequestInit),
    } as never);
    expect(res.status).toBe(413);
  });

  it('download redirects to login without session', async () => {
    routeHarness.cfEnv = {
      SESSION: {},
      DATABASE_URL: 'postgres://test@localhost/test',
      MEDIA: { get: vi.fn() },
    };
    routeHarness.session = null;

    const res = await downloadGet({
      params: { key: 'some-key' },
      request: new Request(`${SITE}/api/download/some-key`),
      cookies: { get: () => undefined, set: vi.fn(), delete: vi.fn() },
    } as never);

    expect(res.status).toBe(302);
    expect(res.headers.get('Location')).toMatch(/\/login\?next=/);
  });

  it('download returns 404 when session has no access grant', async () => {
    routeHarness.cfEnv = {
      SESSION: {},
      DATABASE_URL: 'postgres://test@localhost/test',
      MEDIA: { get: vi.fn(async () => null) },
    };
    routeHarness.session = { userId: 'no-grant-user', role: 'buyer', email: 'nogrant@example.com' };

    const res = await downloadGet({
      params: { key: 'some-key' },
      request: new Request(`${SITE}/api/download/some-key`),
      cookies: { get: () => undefined, set: vi.fn(), delete: vi.fn() },
    } as never);

    expect(res.status).toBe(404);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe('not_found');
  });

  it('download returns 404 when grant exists but R2 object is missing', async () => {
    const userId = 'grant-user-id';
    const ownerKey = `user:${userId}`;
    const blobKey = 'missing-asset.zip';
    await db.execute(sql`
      INSERT INTO access_grant (id, order_id, item_id, owner_key, blob_key)
      VALUES (
        ${crypto.randomUUID()}::uuid,
        ${crypto.randomUUID()}::uuid,
        'line-1',
        ${ownerKey},
        ${blobKey}
      )
    `);

    routeHarness.cfEnv = {
      SESSION: {},
      DATABASE_URL: 'postgres://test@localhost/test',
      MEDIA: { get: vi.fn(async () => null) },
    };
    routeHarness.session = { userId, role: 'buyer', email: 'grant@example.com' };

    const res = await downloadGet({
      params: { key: blobKey },
      request: new Request(`${SITE}/api/download/${encodeURIComponent(blobKey)}`),
      cookies: { get: () => undefined, set: vi.fn(), delete: vi.fn() },
    } as never);

    expect(res.status).toBe(404);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe('not_found');
  });
});
