/**
 * Blueprint composition proof — commerce (preset: registry.json presets.commerce).
 *
 * This is NOT a re-test of each module's surface (billing/ledger/tax/uploads/search each have their own
 * tests). It proves the ONE thing a blueprint exists to prove: that the preset's modules COMPOSE into a
 * real flow and the SEAMS BETWEEN THEM hold. The flow is the product lifecycle — list, then sell:
 *
 *   uploads (is this product image real?) + search (index it)
 *     → tax (split the inclusive price PRE-CHARGE — Pattern C)
 *       → billing.settleCharge (inject the ledger seam — Pattern B)
 *         → ledger (record +gross in the append-only journal)
 *
 * Two headline properties under test:
 *  1. MONEY IDEMPOTENCY (hard floor) — settling the SAME chargeKey twice records exactly ONE journal
 *     entry. billing keys the ledger write `idempotencyKey(['charge', chargeKey])` and ledger
 *     onConflictDoNothing's it, so a provider redelivery / double-submit can never double-credit.
 *  2. CATALOG FAIL-CLOSED (trust boundary) — a forged product image is rejected at the upload gate
 *     BEFORE the product is listed or charged: nothing indexed, nothing in the ledger.
 *
 * Pattern A (invoice-on-charge) is shown as billing config: the provider's `emitsInvoiceOnCharge` flag
 * + the `documentUrls` invoice on the settled result — NOT a `@platform-modules/invoicing` module.
 */
import { beforeEach, describe, expect, it } from 'vitest'
import { settleCharge, type ChargeRequest } from '@platform-modules/billing'
import {
  RejectedImageError,
  validateProductImage,
  type ProductImage,
} from '../src/blueprints/commerce/wiring/uploads'
import { createProductIndex, type ProductIndex } from '../src/blueprints/commerce/wiring/search'
import { resolveInclusiveOrderTax } from '../src/blueprints/commerce/wiring/tax'
import { createCommerceLedger, type CommerceLedger } from '../src/blueprints/commerce/wiring/ledger'
import { createCaptureProvider, type CaptureProvider } from '../src/blueprints/commerce/wiring/billing'
import {
  createCommerceIntentStore,
  type CommerceIntentStore,
} from '../src/blueprints/commerce/wiring/intent'

const MAX_IMAGE_BYTES = 5 * 1024 * 1024
const ORDER_DATE = '2026-06-16' // host-resolved YYYY-MM-DD legal date (Pattern-C date contract)

type CommerceDeps = {
  index: ProductIndex
  ledger: CommerceLedger
  payment: CaptureProvider
  /** Durable charge-intent store (floor #6) — persists across settles to make replay charge-once. */
  intentStore: CommerceIntentStore
}

type ListAndSellInput = {
  productId: string
  title: string
  description: string
  image: ProductImage
  chargeKey: string
  /** Inclusive (tax-included) display price, in agorot. */
  grossAgorot: bigint
}

/**
 * The host-owned glue a real app writes (the blueprint NAMES it; here it lives in the test as the
 * composition under proof). ORDER is the property: the upload trust boundary runs BEFORE any catalog
 * or money side effect, and tax resolves the amount BEFORE billing ever sees it (billing imports zero
 * tax).
 */
async function runListAndSell(deps: CommerceDeps, input: ListAndSellInput) {
  // ── LIST ─────────────────────────────────────────────────────────────────
  // 1. UPLOAD TRUST BOUNDARY — sniff the real content-type + size (throws on a spoof/oversize).
  const mime = validateProductImage(input.image, MAX_IMAGE_BYTES)
  // 2. SEARCH — the product becomes findable through the same searchEntities the host federates over.
  deps.index.add({ id: input.productId, title: input.title, description: input.description })

  // ── SELL ─────────────────────────────────────────────────────────────────
  // 3. TAX (Pattern C) — split the inclusive gross into net + vat PRE-CHARGE; the host owns the amount.
  const tax = resolveInclusiveOrderTax(input.grossAgorot, ORDER_DATE)
  // 4. BILLING → LEDGER (Pattern B) — settle the gross; the injected ledger seam records +gross.
  //    billing's wire amount is integer minor-units (number); tax/ledger are bigint agorot — the host
  //    bridges with Number(gross) at the seam (gross is whole agorot, safely an integer).
  const req: ChargeRequest = {
    chargeKey: input.chargeKey,
    amount: Number(tax.gross),
    currency: 'ILS',
    metadata: { productId: input.productId },
  }
  const charge = await settleCharge(req, {
    provider: deps.payment.provider,
    ledger: deps.ledger.seam,
    db: deps.ledger.db,
    intentStore: deps.intentStore,
  })

  return { mime, tax, charge }
}

/** PNG 8-byte signature + filler — detectMimeFromMagicBytes returns image/png. */
function pngBytes(): Uint8Array {
  return new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d])
}

/** A script-bearing SVG masquerading as an image — magic-bytes rejects dangerous text headers. */
function svgScriptBytes(): Uint8Array {
  return new TextEncoder().encode(
    '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>',
  )
}

describe('blueprint: commerce — list+sell composes uploads/search → tax → billing → ledger (Pattern B/C, money-idempotent)', () => {
  let deps: CommerceDeps
  let index: ProductIndex
  let ledger: CommerceLedger
  let payment: CaptureProvider
  let intentStore: CommerceIntentStore

  beforeEach(async () => {
    index = createProductIndex()
    ledger = await createCommerceLedger()
    payment = createCaptureProvider()
    // Built ONCE per composition (like the ledger): the durable intent persists across settles, so a
    // same-chargeKey replay claims 'settled' and short-circuits before the provider (floor #6).
    intentStore = createCommerceIntentStore()
    deps = { index, ledger, payment, intentStore }
  })

  function validInput(overrides?: Partial<ListAndSellInput>): ListAndSellInput {
    return {
      productId: 'sku-1',
      title: 'Mechanical Keyboard',
      description: 'Hot-swappable, tactile switches.',
      image: { bytes: pngBytes() },
      chargeKey: 'order-1',
      grossAgorot: 11_800n, // ₪118.00 tax-inclusive (₪100.00 net + 18% VAT)
      ...overrides,
    }
  }

  it('valid product: listed (indexed) → taxed inclusive → charged → ledger journal credited +gross, with invoice (Pattern A)', async () => {
    const res = await runListAndSell(deps, validInput())
    expect(res.mime).toBe('image/png')

    // TAX (Pattern C, inclusive) — gross splits into net + vat, and they reconcile to the gross
    expect(res.tax.net).toBe(10_000n)
    expect(res.tax.vat).toBe(1_800n)
    expect(res.tax.net + res.tax.vat).toBe(res.tax.gross)

    // BILLING — settled, and the provider emitted an invoice (Pattern A: emitsInvoiceOnCharge config)
    expect(res.charge.kind).toBe('settled')
    if (res.charge.kind === 'settled') {
      expect(res.charge.amount).toBe(11_800)
      expect(res.charge.documentUrls).toEqual(['https://invoice.test/order-1.pdf'])
    }
    expect(payment.charges).toHaveLength(1)

    // LEDGER (Pattern B) — exactly one journal entry, summing to +gross agorot
    const journal = await ledger.journal()
    expect(journal.count).toBe(1)
    expect(journal.sum).toBe(11_800n)

    // SEARCH — the product is findable
    const found = await index.search('keyboard')
    expect(found.groups[0]?.hits[0]?.id).toBe('sku-1')
  })

  it('money idempotency (hard floor): re-settling the SAME chargeKey records exactly ONE ledger entry', async () => {
    const input = validInput()
    await runListAndSell(deps, input)
    // a provider redelivery / double-submit of the SAME charge — same chargeKey ⇒ same durable intent
    await runListAndSell(deps, input)

    const journal = await ledger.journal()
    expect(journal.count).toBe(1) // onConflictDoNothing collapsed the duplicate — no double-credit
    expect(journal.sum).toBe(11_800n)
    // floor #6 (0587629): the durable intent is already 'settled' on the replay, so settleCharge
    // short-circuits BEFORE the provider — the card is never charged a second time. Idempotency is
    // now enforced AHEAD of the provider call (durable claim), not only behind it at the ledger seam.
    expect(payment.charges).toHaveLength(1)
  })

  it('forged product image (script-bearing SVG): rejected at the upload trust boundary → not listed, not charged', async () => {
    await expect(
      runListAndSell(deps, validInput({ image: { bytes: svgScriptBytes() } })),
    ).rejects.toBeInstanceOf(RejectedImageError)

    // fail-closed — the product never reached the catalog or the money path
    const found = await index.search('keyboard')
    expect(found.groups).toHaveLength(0)
    const journal = await ledger.journal()
    expect(journal.count).toBe(0)
    expect(payment.charges).toHaveLength(0)
  })
})
