import { PGlite } from '@electric-sql/pglite'
import { eq } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/pglite'
import { describe, expect, it, vi } from 'vitest'
import type { TransactionalDatabase } from '@platform-modules/db'
import { withTransactionIdentity } from '@platform-modules/db'
import { appendEntry, ledgerEntries, ledgerSchema } from '@platform-modules/ledger'
import {
  ChargeIntentPendingError,
  confirmRefund,
  confirmSettlement,
  idempotencyKey,
  ingestWebhook,
  InvalidAmountError,
  reconcileCharge,
  RefundExceedsPaidError,
  refundCharge,
  settleCharge,
  toMinorUnits,
  WebhookVerificationError,
  type ChargeRequest,
  type ChargeResult,
  type DedupStore,
  type IntentStore,
  type LedgerSeam,
  type PaymentProvider,
  type ProviderEvent,
  type RefundResult,
} from './index.js'

type TestSchema = typeof ledgerSchema

function asLedgerSeam(): LedgerSeam {
  return { appendEntry: appendEntry as LedgerSeam['appendEntry'] }
}

async function createTestDb(): Promise<TransactionalDatabase<TestSchema>> {
  const client = new PGlite()
  const db = withTransactionIdentity(drizzle(client, { schema: ledgerSchema })) as unknown as TransactionalDatabase<TestSchema>
  await client.exec(`
    CREATE TABLE 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()
    );
    CREATE UNIQUE INDEX ledger_entries_idempotency_key_uq ON ledger_entries (idempotency_key);
  `)
  return db
}

function createFakeDedupStore(
  outcomes: Record<string, 'won' | 'lost'> = {},
): DedupStore & { marked: string[]; claims: string[] } {
  const marked: string[] = []
  const claims: string[] = []
  return {
    marked,
    claims,
    async claim(eventId: string) {
      claims.push(eventId)
      return outcomes[eventId] ?? 'won'
    },
    async markProcessed(eventId: string) {
      marked.push(eventId)
    },
  }
}

type FakeIntent = { status: 'pending' | 'settled'; providerRef?: string; amount: number; currency: string }

function createFakeIntentStore(): IntentStore & {
  intents: Map<string, FakeIntent>
  claims: string[]
  settled: string[]
} {
  const intents = new Map<string, FakeIntent>()
  const claims: string[] = []
  const settled: string[] = []
  return {
    intents,
    claims,
    settled,
    async claimIntent(chargeKey, intent) {
      claims.push(chargeKey)
      const existing = intents.get(chargeKey)
      if (existing) return existing.status === 'settled' ? 'settled' : 'pending'
      intents.set(chargeKey, { status: 'pending', amount: intent.amount, currency: intent.currency })
      return 'won'
    },
    async recordProviderRef(chargeKey, providerRef) {
      const row = intents.get(chargeKey)
      if (row) row.providerRef = providerRef
    },
    async markIntentSettled(chargeKey) {
      settled.push(chargeKey)
      const row = intents.get(chargeKey)
      if (row) row.status = 'settled'
    },
    async listUnsettledIntents(_olderThanMs) {
      return [...intents.entries()]
        .filter(([, v]) => v.status === 'pending')
        .map(([chargeKey, v]) => ({
          chargeKey,
          providerRef: v.providerRef,
          amount: v.amount,
          currency: v.currency,
        }))
    },
  }
}

function createTrackingLedger() {
  const calls: Array<{ key: string; delta: bigint; currency: string | null }> = []
  const insertedKeys = new Set<string>()

  const ledger = {
    async appendEntry(
      _db: unknown,
      input: { key: string; delta: bigint; currency?: string | null; reason: string },
    ) {
      calls.push({ key: input.key, delta: input.delta, currency: input.currency ?? null })
      if (insertedKeys.has(input.key)) return { inserted: false, id: null }
      insertedKeys.add(input.key)
      return { inserted: true, id: `le-${input.key}` }
    },
  }

  return { ledger, calls, insertedKeys }
}

function makeProvider(overrides: Partial<PaymentProvider> & Pick<PaymentProvider, 'provider'>): PaymentProvider {
  return {
    emitsInvoiceOnCharge: false,
    charge: vi.fn(),
    refund: vi.fn(),
    reconcileRefund: vi.fn(async () => ({ kind: 'pending_or_unknown' as const })),
    parseWebhook: vi.fn(),
    ...overrides,
  }
}

describe('idempotencyKey', () => {
  it('is deterministic for the same stable parts', () => {
    const a = idempotencyKey(['charge', 'ord-1'])
    const b = idempotencyKey(['charge', 'ord-1'])
    expect(a).toBe(b)
    expect(a).toBe('charge:ord-1')
  })

  it('is injective when a part contains the separator (no delimiter collision)', () => {
    // Both vectors would collide under a naive join(':') → second appendEntry
    // silently no-ops and overstates the ledger. Encoding keeps them distinct.
    expect(idempotencyKey(['refund', 'ord', '1:r'])).not.toBe(
      idempotencyKey(['refund', 'ord:1', 'r']),
    )
  })
})

describe('toMinorUnits', () => {
  it('coerces integer minor-units to bigint', () => {
    expect(toMinorUnits(1999)).toBe(1999n)
  })

  it('throws on a non-integer amount', () => {
    expect(() => toMinorUnits(19.99)).toThrow(/integer/)
  })
})

describe('settleCharge', () => {
  it('returns clientSecret without posting when requires_client_action', async () => {
    const db = await createTestDb()
    const { ledger, calls } = createTrackingLedger()
    const provider = makeProvider({
      provider: 'stripe',
      charge: vi.fn(async (): Promise<ChargeResult> => ({
        kind: 'requires_client_action',
        chargeKey: 'ord-1',
        providerRef: 'pi_1',
        clientSecret: 'sec_abc',
      })),
    })

    const result = await settleCharge(
      { chargeKey: 'ord-1', amount: 5000, currency: 'usd' },
      { provider, ledger, db, intentStore: createFakeIntentStore() },
    )

    expect(result).toMatchObject({ kind: 'requires_client_action', clientSecret: 'sec_abc' })
    expect(calls).toHaveLength(0)
  })

  it('funnels settled charges once with a namespaced ledger key', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const provider = makeProvider({
      provider: 'stripe',
      charge: vi.fn(async (req: ChargeRequest): Promise<ChargeResult> => ({
        kind: 'settled',
        chargeKey: req.chargeKey,
        providerRef: 'stripe-1',
        amount: req.amount,
        currency: 'USD',
      })),
    })

    const intentStore = createFakeIntentStore()
    await settleCharge({ chargeKey: 'ord-2', amount: 1200, currency: 'ils' }, { provider, ledger, db, intentStore })
    // Second call replays the SAME chargeKey: the intent is already 'settled', so
    // it short-circuits WITHOUT re-calling the provider (no double-charge) and the
    // ledger still holds exactly one entry.
    await settleCharge({ chargeKey: 'ord-2', amount: 1200, currency: 'ils' }, { provider, ledger, db, intentStore })

    expect(provider.charge).toHaveBeenCalledTimes(1)
    const rows = await db.select().from(ledgerEntries)
    expect(rows).toHaveLength(1)
    expect(rows[0]?.idempotencyKey).toBe(idempotencyKey(['charge', 'ord-2']))
    expect(rows[0]?.delta).toBe(1200n)
  })
})

describe('settleCharge durable charge-intent (floor #6 — closes the charge→ledger window)', () => {
  it('claims a durable intent BEFORE calling the provider', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const intentStore = createFakeIntentStore()
    const order: string[] = []
    const provider = makeProvider({
      provider: 'stripe',
      charge: vi.fn(async (req: ChargeRequest): Promise<ChargeResult> => {
        order.push('charge')
        return {
          kind: 'settled',
          chargeKey: req.chargeKey,
          providerRef: 'pi_x',
          amount: req.amount,
          currency: 'USD',
        }
      }),
    })
    // record claim order
    const realClaim = intentStore.claimIntent.bind(intentStore)
    intentStore.claimIntent = async (k, i) => {
      order.push('claim')
      return realClaim(k, i)
    }

    await settleCharge(
      { chargeKey: 'ord-int-1', amount: 5000, currency: 'ils' },
      { provider, ledger, db, intentStore },
    )

    // The claim must precede the provider call — that is what makes a crash
    // between charge and ledger-post leave a recoverable record, not a silent charge.
    expect(order).toEqual(['claim', 'charge'])
    expect(intentStore.settled).toEqual(['ord-int-1'])
    expect(intentStore.intents.get('ord-int-1')?.providerRef).toBe('pi_x')
  })

  it('leaves a RECOVERABLE pending intent (no ledger post) when the ledger crashes after the provider charged', async () => {
    const db = await createTestDb()
    const intentStore = createFakeIntentStore()
    const provider = makeProvider({
      provider: 'stripe',
      charge: vi.fn(async (req: ChargeRequest): Promise<ChargeResult> => ({
        kind: 'settled',
        chargeKey: req.chargeKey,
        providerRef: 'pi_crash',
        amount: req.amount,
        currency: 'USD',
      })),
    })
    const crashingLedger: LedgerSeam = {
      appendEntry: (async () => {
        throw new Error('worker evicted during ledger post')
      }) as LedgerSeam['appendEntry'],
    }

    await expect(
      settleCharge(
        { chargeKey: 'ord-crash', amount: 4200, currency: 'ils' },
        { provider, ledger: crashingLedger, db, intentStore },
      ),
    ).rejects.toThrow(/ledger post/)

    // The window is CLOSED: the charge happened, but a durable pending intent
    // (carrying the provider ref recorded the instant charge returned) survives —
    // a recoverable record, NOT a silent charge-without-record.
    const row = intentStore.intents.get('ord-crash')
    expect(row?.status).toBe('pending')
    expect(row?.providerRef).toBe('pi_crash')
    // No ledger entry posted (the crash was the post itself).
    expect(intentStore.settled).toHaveLength(0)
  })

  it('refuses to re-charge a chargeKey with an in-flight/stuck pending intent', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const intentStore = createFakeIntentStore()
    // Seed a stuck pending intent (a prior crash before settle).
    await intentStore.claimIntent('ord-stuck', { amount: 1000, currency: 'ils' })

    const provider = makeProvider({
      provider: 'stripe',
      charge: vi.fn(async (req: ChargeRequest): Promise<ChargeResult> => ({
        kind: 'settled',
        chargeKey: req.chargeKey,
        providerRef: 'pi_dup',
        amount: req.amount,
        currency: 'USD',
      })),
    })

    await expect(
      settleCharge(
        { chargeKey: 'ord-stuck', amount: 1000, currency: 'ils' },
        { provider, ledger, db, intentStore },
      ),
    ).rejects.toBeInstanceOf(ChargeIntentPendingError)
    // Critically: the provider was NOT called again (no double-charge).
    expect(provider.charge).not.toHaveBeenCalled()
  })

  it('short-circuits a replay of an already-settled chargeKey without calling the provider', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const intentStore = createFakeIntentStore()
    await intentStore.claimIntent('ord-done', { amount: 800, currency: 'ils' })
    await intentStore.markIntentSettled('ord-done')

    const provider = makeProvider({ provider: 'stripe', charge: vi.fn() })
    const result = await settleCharge(
      { chargeKey: 'ord-done', amount: 800, currency: 'ils' },
      { provider, ledger, db, intentStore },
    )

    expect(result).toMatchObject({ kind: 'settled', chargeKey: 'ord-done' })
    expect(provider.charge).not.toHaveBeenCalled()
  })
})

describe('reconcileCharge (M4 active-sweep recovery — never re-charges)', () => {
  it('settles a stuck pending intent from the host resolver outcome, without re-calling the provider', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const intentStore = createFakeIntentStore()
    // A crashed charge: pending intent with a recorded provider ref.
    await intentStore.claimIntent('ord-recover', { amount: 3300, currency: 'ils' })
    await intentStore.recordProviderRef('ord-recover', 'pi_recovered')

    // The host resolver reads the provider/order: the charge DID settle.
    const resolveChargeOutcome = vi.fn(async (chargeKey: string) =>
      chargeKey === 'ord-recover'
        ? { settled: true, providerRef: 'pi_recovered', amount: 3300, currency: 'USD' }
        : null,
    )

    const stale = await intentStore.listUnsettledIntents(0)
    expect(stale.map((i) => i.chargeKey)).toEqual(['ord-recover'])

    const action = await reconcileCharge('ord-recover', {
      intentStore,
      resolveChargeOutcome,
      ledger,
      db,
    })

    expect(action).toBe('settled')
    // The ledger now records the charge (window healed), idempotently keyed.
    const rows = await db.select().from(ledgerEntries)
    expect(rows).toHaveLength(1)
    expect(rows[0]?.idempotencyKey).toBe(idempotencyKey(['charge', 'ord-recover']))
    expect(rows[0]?.delta).toBe(3300n)
    expect(intentStore.intents.get('ord-recover')?.status).toBe('settled')
    // No further provider charge call exists on this path — reconcile only READS.
  })

  it('leaves the intent pending (unresolved) when the resolver cannot confirm a settled charge', async () => {
    const db = await createTestDb()
    const { ledger, calls } = createTrackingLedger()
    const intentStore = createFakeIntentStore()
    await intentStore.claimIntent('ord-unknown', { amount: 500, currency: 'ils' })

    // Resolver returns null (outcome not yet knowable) or unsettled.
    const action = await reconcileCharge('ord-unknown', {
      intentStore,
      resolveChargeOutcome: vi.fn(async () => null),
      ledger,
      db,
    })

    expect(action).toBe('unresolved')
    expect(calls).toHaveLength(0) // no ledger post on an unresolved outcome
    expect(intentStore.intents.get('ord-unknown')?.status).toBe('pending') // stays for next sweep
  })
})

describe('confirmSettlement replay', () => {
  it('no-ops on a replayed settlement for the same chargeKey', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const settlement = { amount: 900, currency: 'USD', providerRef: 'pi_x' }

    await confirmSettlement('ord-3', settlement, { ledger, db })
    await confirmSettlement('ord-3', settlement, { ledger, db })

    const rows = await db.select().from(ledgerEntries)
    expect(rows).toHaveLength(1)
  })

  it('threads settlement currency into the ledger append call', async () => {
    const db = await createTestDb()
    const { ledger, calls } = createTrackingLedger()

    await confirmSettlement('ord-3-currency', { amount: 900, currency: 'USD', providerRef: 'pi_x' }, { ledger, db })

    expect(calls).toEqual([
      expect.objectContaining({
        key: idempotencyKey(['charge', 'ord-3-currency']),
        delta: 900n,
        currency: 'USD',
      }),
    ])
  })

  it('rejects a negative settlement amount — a settlement never debits the ledger', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()

    await expect(
      confirmSettlement('ord-neg', { amount: -100, currency: 'USD', providerRef: 'pi_neg' }, { ledger, db }),
    ).rejects.toThrow(/non-negative/)
    await expect(
      confirmSettlement('ord-neg', { amount: -100, currency: 'USD', providerRef: 'pi_neg' }, { ledger, db }),
    ).rejects.toThrow(InvalidAmountError)

    const rows = await db.select().from(ledgerEntries)
    expect(rows).toHaveLength(0)
  })
})

describe('ingestWebhook settlement path', () => {
  it('posts once via confirmSettlement and skips duplicate settlement webhooks', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const dedup = createFakeDedupStore()
    const dispatch = vi.fn()
    const event: ProviderEvent = {
      eventId: 'evt_settle_1',
      kind: 'settlement',
      chargeKey: 'ord-4',
      providerRef: 'pi_4',
      amount: 2500,
      currency: 'USD',
    }
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(async () => event),
    })

    const req = new Request('https://host/webhook', { method: 'POST', body: '{"id":"evt"}' })
    await ingestWebhook(req, { provider, dedupStore: dedup, dispatch, ledger, db })

    const req2 = new Request('https://host/webhook', { method: 'POST', body: '{"id":"evt"}' })
    await ingestWebhook(req2, { provider, dedupStore: dedup, dispatch, ledger, db })

    const rows = await db.select().from(ledgerEntries)
    expect(rows).toHaveLength(1)
    expect(dispatch).toHaveBeenCalledTimes(2)
  })

  it('converges sync settle and a late webhook to one ledger post', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const provider = makeProvider({
      provider: 'stripe',
      charge: vi.fn(async (): Promise<ChargeResult> => ({
        kind: 'settled',
        chargeKey: 'ord-5',
        providerRef: 's5',
        amount: 3000,
        currency: 'USD',
      })),
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_late',
          kind: 'settlement',
          chargeKey: 'ord-5',
          providerRef: 'pi_5',
          amount: 3000,
          currency: 'USD',
        }),
      ),
    })

    await settleCharge(
      { chargeKey: 'ord-5', amount: 3000, currency: 'ils' },
      { provider, ledger, db, intentStore: createFakeIntentStore() },
    )

    const dedup = createFakeDedupStore()
    const dispatch = vi.fn()
    const req = new Request('https://host/webhook', { method: 'POST', body: '{}' })
    await ingestWebhook(req, { provider, dedupStore: dedup, dispatch, ledger, db })

    const rows = await db.select().from(ledgerEntries)
    expect(rows).toHaveLength(1)
  })
})

describe('ingestWebhook M5 claim floor', () => {
  it('returns 200 without dispatch or ledger post when claim is lost', async () => {
    const db = await createTestDb()
    const { ledger, calls } = createTrackingLedger()
    const dedup = createFakeDedupStore({ evt_lost: 'lost' })
    const dispatch = vi.fn()
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_lost',
          kind: 'settlement',
          chargeKey: 'ord-6',
          providerRef: 'pi_6',
          amount: 1000,
          currency: 'USD',
        }),
      ),
    })

    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch, ledger, db },
    )

    expect(res.status).toBe(200)
    expect(dispatch).not.toHaveBeenCalled()
    expect(calls).toHaveLength(0)
  })

  it('dispatches once and marks processed for the winning claim', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const dedup = createFakeDedupStore({ evt_win: 'won' })
    const dispatch = vi.fn()
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_win',
          kind: 'other',
          raw: { type: 'customer.created' },
        }),
      ),
    })

    await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch, ledger, db },
    )

    expect(dispatch).toHaveBeenCalledTimes(1)
    expect(dedup.marked).toEqual(['evt_win'])
  })
})

describe('ingestWebhook M6 release floor', () => {
  it('leaves the claim unmarked when dispatch throws after its effect', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const dedup = createFakeDedupStore()
    const dispatch = vi.fn(async () => {
      throw new Error('dispatch failed after effect')
    })
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_m6',
          kind: 'other',
          raw: {},
        }),
      ),
    })

    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch, ledger, db },
    )

    expect(res.status).toBe(500)
    expect(dedup.marked).toHaveLength(0)
  })
})

describe('ingestWebhook owns-its-Response floor', () => {
  // The MODULE-OWNED post (confirmSettlement/confirmRefund after a won claim) runs
  // before dispatch. A transient failure there MUST be caught → 500, claim left
  // UNMARKED (TTL-heals; the post is idempotent on its key). ingestWebhook NEVER
  // lets a throw escape — it is the web-standard Request→Response primitive.
  function throwingLedger(): LedgerSeam {
    return {
      appendEntry: (async () => {
        throw new Error('ledger/DB hiccup during module-owned post')
      }) as LedgerSeam['appendEntry'],
    }
  }

  it('returns 500 (not a throw) and leaves the claim unmarked when the settlement post throws', async () => {
    const db = await createTestDb()
    const ledger = throwingLedger()
    const dedup = createFakeDedupStore()
    const dispatch = vi.fn()
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_post_settle',
          kind: 'settlement',
          chargeKey: 'ord-post-1',
          providerRef: 'pi_post_1',
          amount: 1500,
          currency: 'USD',
        }),
      ),
    })

    // Direct await: a thrown exception would propagate and fail the test — that IS
    // the "must not throw" assertion.
    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch, ledger, db },
    )

    expect(res.status).toBe(500)
    expect(dedup.marked).toHaveLength(0)
    expect(dispatch).not.toHaveBeenCalled()
  })

  it('dispatches a refund event without posting a provider-directed ledger reversal', async () => {
    const db = await createTestDb()
    const ledger = throwingLedger()
    const dedup = createFakeDedupStore()
    const dispatch = vi.fn()
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_post_refund',
          kind: 'refund',
          refundKey: idempotencyKey(['refund', 'ord-post-2', 're_x']),
          chargeKey: 'ord-post-2',
          providerChargeId: 'pi_post_2',
          providerRef: 're_x',
          amount: 700,
          currency: 'USD',
        }),
      ),
    })

    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch, ledger, db },
    )

    expect(res.status).toBe(200)
    expect(dedup.marked).toEqual(['evt_post_refund'])
    expect(dispatch).toHaveBeenCalledTimes(1)
  })

  it('returns 500 (not a throw) and posts nothing when the dedup claim throws', async () => {
    const db = await createTestDb()
    const { ledger, calls } = createTrackingLedger()
    const dispatch = vi.fn()
    const dedup: DedupStore & { marked: string[] } = {
      marked: [],
      async claim() {
        throw new Error('dedup store unreachable')
      },
      async markProcessed(eventId: string) {
        this.marked.push(eventId)
      },
    }
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_claim_throw',
          kind: 'settlement',
          chargeKey: 'ord-post-3',
          providerRef: 'pi_post_3',
          amount: 1000,
          currency: 'USD',
        }),
      ),
    })

    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch, ledger, db },
    )

    expect(res.status).toBe(500)
    expect(calls).toHaveLength(0)
    expect(dispatch).not.toHaveBeenCalled()
    expect(dedup.marked).toHaveLength(0)
  })

  it('returns 200 (not a throw, not 500) when markProcessed throws after post+dispatch succeeded', async () => {
    // markProcessed-200 floor: the FINAL await runs AFTER the module-owned post AND
    // dispatch already succeeded — the event WAS fully processed, so 200 is truthful
    // and a throw here must NOT escape (the last hole in the never-throws invariant).
    const db = await createTestDb()
    const { ledger, calls } = createTrackingLedger()
    const dispatch = vi.fn()
    const dedup: DedupStore & { marked: string[] } = {
      marked: [],
      async claim() {
        return 'won'
      },
      async markProcessed() {
        throw new Error('markProcessed store write failed')
      },
    }
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(
        async (): Promise<ProviderEvent> => ({
          eventId: 'evt_mark_throw',
          kind: 'settlement',
          chargeKey: 'ord-mark-1',
          providerRef: 'pi_mark_1',
          amount: 2200,
          currency: 'USD',
        }),
      ),
    })

    // Direct await: a thrown exception would propagate and fail the test — that IS
    // the "must not throw" assertion for the markProcessed path.
    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch, ledger, db },
    )

    expect(res.status).toBe(200)
    // The effects DID complete (truthful 200, not a spurious 500): the post landed
    // and dispatch ran exactly once — this pins the markProcessed-threw path, not
    // merely that some 200 happened.
    expect(calls).toHaveLength(1)
    expect(dispatch).toHaveBeenCalledTimes(1)
    // markProcessed threw before recording → processed_at stays NULL (the residual).
    expect(dedup.marked).toHaveLength(0)
  })
})

describe('namespacing floor', () => {
  it('does not treat a colliding bare chargeKey entry as already settled', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()

    await appendEntry(db, { key: 'ord-7', delta: 1n, reason: 'foreign-writer' })

    await confirmSettlement('ord-7', { amount: 500, currency: 'USD', providerRef: 'pi_7' }, { ledger, db })

    const rows = await db.select().from(ledgerEntries)
    const billingRow = rows.find((r) => r.idempotencyKey === idempotencyKey(['charge', 'ord-7']))
    expect(billingRow).toBeDefined()
    expect(billingRow?.delta).toBe(500n)
    expect(rows).toHaveLength(2)
  })
})

describe('refundCharge and confirmRefund', () => {
  it('posts a reversing entry once with the refundKey', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const refundKey = idempotencyKey(['refund', 'ord-8', 'r1'])

    await confirmSettlement('ord-8', { amount: 2000, currency: 'USD', providerRef: 'pi_8' }, { ledger, db })

    const provider = makeProvider({
      provider: 'stripe',
      refund: vi.fn(async (): Promise<RefundResult> => ({
        kind: 'refunded',
        refundKey,
        chargeKey: 'ord-8',
        providerRef: 'ref_1',
        amount: 500,
        currency: 'USD',
      })),
    })

    await refundCharge(
      { refundKey, chargeKey: 'ord-8', refundId: 'r1', amount: 500 },
      { provider, ledger, db },
    )
    await refundCharge(
      { refundKey, chargeKey: 'ord-8', refundId: 'r1', amount: 500 },
      { provider, ledger, db },
    )

    const rows = await db
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, refundKey))
    expect(rows).toHaveLength(1)
    expect(rows[0]?.delta).toBe(-500n)
  })

  it('threads refund currency into the ledger append call', async () => {
    const db = await createTestDb()
    const { ledger, calls } = createTrackingLedger()
    const refundKey = idempotencyKey(['refund', 'ord-8', 'r-currency'])

    await confirmRefund(refundKey, { chargeKey: 'ord-8', amount: 500, currency: 'USD' }, { ledger, db })

    expect(calls).toEqual([
      expect.objectContaining({
        key: refundKey,
        delta: -500n,
        currency: 'USD',
      }),
    ])
  })

  it('does not double-refund when the provider replays the same idempotency key', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const refundKey = idempotencyKey(['refund', 'ord-9', 'r1'])
    await confirmSettlement('ord-9', { amount: 1000, currency: 'USD', providerRef: 'pi_9' }, { ledger, db })

    let providerCalls = 0
    const provider = makeProvider({
      provider: 'stripe',
      refund: vi.fn(async (): Promise<RefundResult> => {
        providerCalls += 1
        return {
          kind: 'refunded',
          refundKey,
          chargeKey: 'ord-9',
          providerRef: 'ref_9',
          amount: 200,
          currency: 'USD',
        }
      }),
    })

    await refundCharge(
      { refundKey, chargeKey: 'ord-9', refundId: 'r1', amount: 200 },
      { provider, ledger, db },
    )
    await refundCharge(
      { refundKey, chargeKey: 'ord-9', refundId: 'r1', amount: 200 },
      { provider, ledger, db },
    )

    expect(providerCalls).toBe(2)
    const refundRows = await db
      .select()
      .from(ledgerEntries)
      .where(eq(ledgerEntries.idempotencyKey, refundKey))
    expect(refundRows).toHaveLength(1)
  })
})

describe('RefundExceedsPaidError (M8)', () => {
  // Billing rejects ONLY the violation it can see WITHOUT reading ledger state —
  // a non-positive / non-integer amount. Settled-charge existence + the amount cap
  // are HOST preconditions (spec floor #8); billing never queries ledger to validate.
  it('rejects non-positive refund amounts', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const refundKey = idempotencyKey(['refund', 'ord-10', 'r1'])

    await expect(
      confirmRefund(refundKey, { chargeKey: 'ord-10', amount: 0, currency: 'USD' }, { ledger, db }),
    ).rejects.toMatchObject({
      code: 'REFUND_EXCEEDS_PAID',
      httpStatus: 422,
    })
  })

  it('rejects a non-integer refund amount', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const refundKey = idempotencyKey(['refund', 'ord-11', 'r1'])

    await expect(
      confirmRefund(refundKey, { chargeKey: 'ord-11', amount: 1.5, currency: 'USD' }, { ledger, db }),
    ).rejects.toBeInstanceOf(RefundExceedsPaidError)
  })
})

describe('ingestWebhook parseWebhook error-class', () => {
  it('returns 400 only for WebhookVerificationError', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const dedup = createFakeDedupStore()
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(async () => {
        throw new WebhookVerificationError('bad signature')
      }),
    })

    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch: vi.fn(), ledger, db },
    )

    expect(res.status).toBe(400)
    expect(dedup.claims).toHaveLength(0)
  })

  it('returns 500 for any other parseWebhook throw', async () => {
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const dedup = createFakeDedupStore()
    const provider = makeProvider({
      provider: 'stripe',
      parseWebhook: vi.fn(async () => {
        throw new Error('paymentIntents.retrieve failed')
      }),
    })

    const res = await ingestWebhook(
      new Request('https://host/webhook', { method: 'POST', body: '{}' }),
      { provider, dedupStore: dedup, dispatch: vi.fn(), ledger, db },
    )

    expect(res.status).toBe(500)
    expect(dedup.claims).toHaveLength(0)
  })
})

describe('ingestWebhook raw-bytes guard', () => {
  it('returns retryable 500 (never a drop-it 400) when the body was already consumed', async () => {
    // A pre-consumed body is a HOST misconfiguration, not a forgery. 400 tells the
    // provider to stop retrying → every event in the misconfig window is PERMANENTLY
    // dropped (silent under-post). 500 keeps the provider retrying so the backlog
    // delivers once the host is fixed — the spec error-class rule: 400 is reserved
    // for the typed verification failure, everything else is retryable.
    const db = await createTestDb()
    const ledger = asLedgerSeam()
    const dedup = createFakeDedupStore()
    const provider = makeProvider({ provider: 'stripe' })
    const req = new Request('https://host/webhook', { method: 'POST', body: '{}' })
    await req.text()

    const res = await ingestWebhook(req, {
      provider,
      dedupStore: dedup,
      dispatch: vi.fn(),
      ledger,
      db,
    })

    expect(res.status).toBe(500)
  })
})
