/**
 * Blueprint composition proof — marketplace (the TWO-PRESET COMPOSE of commerce ∪ community,
 * delivery-stack §4.1.1). There is NO `presets.marketplace`; marketplace is the UNION of the two
 * presets that each already have a hand-built blueprint, so this test REUSES their wiring seams
 * verbatim (importing `../commerce/wiring/*` + `../community/wiring/*`) and authors ONLY the
 * compose-distinct seams (`../marketplace/wiring/{split,realtime}`). The reuse IS the compose proof:
 * the REAL preset seams compose, not forked copies.
 *
 * The headline marketplace-distinct property is the MULTI-PARTY SPLIT — one buyer charge is allocated
 * across the seller + the platform as TWO idempotent ledger entries that sum to the gross (money hard
 * floor: nothing created/destroyed). The flow, spanning BOTH presets:
 *
 *   community/auth (may this buyer order?) ─ GATE
 *     → commerce/tax (split inclusive gross PRE-CHARGE, Pattern C)
 *       → commerce/billing PROVIDER.charge (the buyer pays the gross — NOT settleCharge: the
 *          single-party funnel would record +gross; a marketplace needs the domain split instead)
 *         → marketplace/split (TWO ledger entries: seller payout + platform take, summing to gross)
 *           → community/audit (record the settled order)
 *             → community/notifications (notify the SELLER — email channel composes mail)
 *               → marketplace/realtime (seller-PRIVATE order-settled envelope, targetUserId-scoped)
 *
 * Three cases: full union compose + split · split-and-comms idempotency · unauthorized buyer
 * (fail-closed across BOTH presets). The catalog trust boundary (uploads) is proven in the commerce +
 * community blueprints already; here uploads/search are exercised as the listing preamble (touched, to
 * prove the full union composes) without re-testing that boundary.
 */
import { beforeEach, describe, expect, it } from 'vitest'
import {
  PermissionDeniedError,
  getSession,
  requirePermission,
  type AuthEngine,
  type Principal,
} from '@platform-modules/auth'
import { listAudit, logAudit } from '@platform-modules/audit'
import { type ChargeRequest } from '@platform-modules/billing'
// ── reused commerce wiring (the commerce half of the union — imported, NOT re-authored) ──
import { 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'
// ── reused community wiring (the community half of the union — imported, NOT re-authored) ──
import { bearer, createFakeAuthEngine } from '../src/blueprints/community/wiring/auth'
import { createCaptureMail, type CaptureMail } from '../src/blueprints/community/wiring/mail'
import {
  createFollowerNotifications,
  type FollowerNotifications,
} from '../src/blueprints/community/wiring/notifications'
import { createAuditDb, type AuditDb } from '../src/blueprints/community/wiring/audit'
// ── authored compose-distinct wiring (the ONLY new seams — §4.1.1) ──
import {
  computeSplit,
  readSplitEntries,
  recordMarketplaceSplit,
} from '../src/blueprints/marketplace/wiring/split'
import { orderSettledEvent, type OrderSettledEvent } from '../src/blueprints/marketplace/wiring/realtime'

const MAX_IMAGE_BYTES = 5 * 1024 * 1024
const ORDER_DATE = '2026-06-16' // host-resolved YYYY-MM-DD legal date (Pattern-C date contract)
const COMMISSION_BPS = 1000 // platform commission = 10.00% of net

type MarketplaceDeps = {
  authEngine: AuthEngine
  index: ProductIndex
  ledger: CommerceLedger
  payment: CaptureProvider
  mail: CaptureMail
  notifications: FollowerNotifications
  auditDb: AuditDb
}

type ListInput = { productId: string; title: string; description: string; image: ProductImage }

type PurchaseInput = {
  buyerHeaders: Headers
  orderId: string
  productId: string
  sellerId: string
  sellerEmail: string
  /** Inclusive (tax-included) order total, in agorot. */
  grossAgorot: bigint
  // host-supplied envelope identity (kept out of the realtime seam — see wiring/realtime.ts)
  eventId: string
  timestamp: string
}

/** Seller action — list a product (touches uploads + search). Kept OUT of beforeEach so the
 *  unauthorized-buyer case runs on a clean slate and its zero-side-effects asserts stay unmuddied. */
function listProduct(deps: MarketplaceDeps, input: ListInput): string {
  const mime = validateProductImage(input.image, MAX_IMAGE_BYTES) // catalog trust boundary
  deps.index.add({ id: input.productId, title: input.title, description: input.description })
  return mime
}

/**
 * The host-owned glue a real marketplace writes (the blueprint NAMES it; here it lives in the test as
 * the composition under proof). The auth GATE runs FIRST — before any charge, split, or notice — so
 * an unauthorized buyer produces zero side effects across BOTH presets (the fail-closed property).
 */
async function runMarketplacePurchase(deps: MarketplaceDeps, input: PurchaseInput) {
  // 1. AUTH GATE (community) — resolve the buyer, require the order capability (throws if absent).
  const principal: Principal | null = await getSession(input.buyerHeaders, deps.authEngine)
  const buyer = requirePermission('order:create')(principal)

  // 2. TAX (commerce, Pattern C) — split the inclusive gross into net + vat BEFORE the charge.
  const tax = resolveInclusiveOrderTax(input.grossAgorot, ORDER_DATE)

  // 3. BILLING PROVIDER (commerce) — the buyer pays the gross. We call the PROVIDER seam directly, NOT
  //    billing.settleCharge: settleCharge funnels a single +gross into one ledger entry, but a
  //    marketplace must SPLIT the proceeds — so the host composes provider.charge + its own ledger
  //    split (the split is domain, extract-skill §2). Real provider idempotency is the host's job via
  //    `chargeKey`; the fake records every call.
  const chargeReq: ChargeRequest = {
    chargeKey: input.orderId,
    amount: Number(tax.gross),
    currency: 'ILS',
    metadata: { orderId: input.orderId, sellerId: input.sellerId, productId: input.productId },
  }
  const charge = await deps.payment.provider.charge(chargeReq)

  // 4. SPLIT (marketplace compose-distinct) — allocate gross across seller + platform; record TWO
  //    idempotent ledger entries that sum to gross.
  const split = computeSplit({ net: tax.net, vat: tax.vat, commissionBps: COMMISSION_BPS })
  const recorded = await recordMarketplaceSplit(deps.ledger.db, {
    orderId: input.orderId,
    sellerId: input.sellerId,
    split,
  })

  // 5. AUDIT (community) — record the settled order (logAudit never throws; decoupled post-commit).
  await logAudit(deps.auditDb, {
    actorId: buyer.userId,
    actorType: 'user',
    action: 'order.settle',
    entityType: 'order',
    entityId: input.orderId,
    metadata: {
      sellerId: input.sellerId,
      gross: String(split.gross),
      sellerNet: String(split.sellerNet),
      platformTake: String(split.platformTake),
    },
  })

  // 6. NOTIFICATIONS (community) — notify the SELLER (one recipient; email channel composes mail).
  //    The event id is keyed on the order so a re-purchase dedups (idempotency spans comms too).
  await deps.notifications.fanOut({
    id: `order:${input.orderId}`,
    type: 'order.sold',
    userId: input.sellerId,
    template: { subject: 'You made a sale', body: '{{buyer}} bought {{product}} — ₪{{net}} agorot is yours' },
    data: { buyer: buyer.userId, product: input.productId, net: String(split.sellerNet) },
    recipients: { inapp: input.sellerId, email: input.sellerEmail },
  })

  // 7. REALTIME (marketplace compose-distinct) — seller-PRIVATE settled envelope (targetUserId-scoped).
  const realtime: OrderSettledEvent = orderSettledEvent({
    id: input.eventId,
    sellerId: input.sellerId,
    timestamp: input.timestamp,
    payload: { orderId: input.orderId, productId: input.productId, sellerNet: String(split.sellerNet) },
  })

  return { tax, charge, split, recorded, realtime }
}

/** 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])
}

describe('blueprint: marketplace — commerce ∪ community compose; one charge SPLITS across seller + platform (idempotent, fail-closed)', () => {
  let deps: MarketplaceDeps
  let mail: CaptureMail

  beforeEach(async () => {
    // Buyers are single-scope principals (community auth): one may order, one may not.
    const buyerPrincipal: Principal = {
      userId: 'u-buyer',
      sessionId: 's-buyer',
      roles: ['buyer'],
      capabilities: ['order:create', 'catalog:read'],
    }
    const browserPrincipal: Principal = {
      userId: 'u-browser',
      sessionId: 's-browser',
      roles: ['visitor'],
      capabilities: ['catalog:read'],
    }
    mail = createCaptureMail()
    deps = {
      authEngine: createFakeAuthEngine(
        new Map([
          ['tok-buyer', buyerPrincipal],
          ['tok-browser', browserPrincipal],
        ]),
      ),
      index: createProductIndex(),
      ledger: await createCommerceLedger(),
      payment: createCaptureProvider(),
      mail,
      // The §4.1.1 sanctioned reuse-edit: community's notifications seam, parameterized for the
      // 'order.sold' event (the ONLY change reuse required — the convergence signal).
      notifications: createFollowerNotifications(mail.mail, { 'order.sold': ['inapp', 'email'] }),
      auditDb: await createAuditDb(),
    }
  })

  function validList(): ListInput {
    return {
      productId: 'sku-9',
      title: 'Handmade Ceramic Mug',
      description: 'Wheel-thrown stoneware, food-safe glaze.',
      image: { bytes: pngBytes() },
    }
  }

  function validPurchase(overrides?: Partial<PurchaseInput>): PurchaseInput {
    return {
      buyerHeaders: bearer('tok-buyer'),
      orderId: 'order-9',
      productId: 'sku-9',
      sellerId: 'u-seller',
      sellerEmail: 'seller@marketplace.test',
      grossAgorot: 11_800n, // ₪118.00 inclusive (₪100.00 net + 18% VAT)
      eventId: 'evt-9',
      timestamp: '2026-06-16T00:00:00.000Z',
      ...overrides,
    }
  }

  it('valid buyer + listed product: charged once, SPLIT into seller payout + platform take (sum = gross), seller notified + audited + private realtime envelope', async () => {
    expect(listProduct(deps, validList())).toBe('image/png')

    const res = await runMarketplacePurchase(deps, validPurchase())

    // TAX (Pattern C) — inclusive gross splits into net + vat
    expect(res.tax.net).toBe(10_000n)
    expect(res.tax.vat).toBe(1_800n)

    // SPLIT — the marketplace-distinct property: one charge allocated across two parties, summing to gross
    expect(res.split.sellerNet).toBe(9_000n) // net 10000 − 10% commission (1000)
    expect(res.split.platformTake).toBe(2_800n) // commission 1000 + vat 1800
    expect(res.split.sellerNet + res.split.platformTake).toBe(res.split.gross) // money floor: nothing lost
    expect(res.split.gross).toBe(11_800n)

    // BILLING — the buyer paid the gross once, with a provider invoice (Pattern A)
    expect(res.charge.kind).toBe('settled')
    if (res.charge.kind === 'settled') expect(res.charge.amount).toBe(11_800)
    expect(deps.payment.charges).toHaveLength(1)

    // LEDGER — exactly TWO journal entries (the split), summing to the gross
    const journal = await deps.ledger.journal()
    expect(journal.count).toBe(2)
    expect(journal.sum).toBe(11_800n)
    // …and the breakdown PERSISTED with the right party tags (not just computed in memory)
    const entries = await readSplitEntries(deps.ledger.db)
    const sellerEntry = entries.find((e) => e.reason === 'seller_payout')
    const platformEntry = entries.find((e) => e.reason === 'platform_take')
    expect(sellerEntry?.delta).toBe(9_000n)
    expect(sellerEntry?.ref?.party).toBe('seller')
    expect(platformEntry?.delta).toBe(2_800n)
    expect(platformEntry?.ref?.party).toBe('platform')

    // AUDIT — one settled-order row attributed to the buyer
    const page = await listAudit(deps.auditDb, { entityType: 'order', action: 'order.settle' })
    expect(page.items).toHaveLength(1)
    expect(page.items[0]?.actorId).toBe('u-buyer')

    // NOTIFICATIONS — the SELLER got one inapp + one email; the email carries their net payout
    expect(deps.notifications.inApp).toHaveLength(1)
    expect(deps.notifications.inApp[0]?.recipient).toBe('u-seller')
    expect(mail.sent).toHaveLength(1)
    expect(mail.sent[0]?.to).toBe('seller@marketplace.test')
    expect(mail.sent[0]?.subject).toBe('You made a sale')
    expect(mail.sent[0]?.text).toContain('9000')

    // REALTIME — seller-PRIVATE envelope: targetUserId pins delivery to the seller alone
    expect(res.realtime.type).toBe('order.settled')
    expect(res.realtime.targetUserId).toBe('u-seller')
    expect(res.realtime.payload.sellerNet).toBe('9000')

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

  it('idempotency across split AND comms: re-purchasing the SAME order leaves two ledger entries and one seller email', async () => {
    listProduct(deps, validList())
    await runMarketplacePurchase(deps, validPurchase())
    const second = await runMarketplacePurchase(deps, validPurchase()) // provider redelivery / double-submit

    // LEDGER — still TWO entries (onConflictDoNothing collapsed the re-allocation), not four
    const journal = await deps.ledger.journal()
    expect(journal.count).toBe(2)
    expect(journal.sum).toBe(11_800n)
    expect(second.recorded.sellerInserted).toBe(false) // the re-run inserted nothing
    expect(second.recorded.platformInserted).toBe(false)

    // COMMS — the seller was notified ONCE; the second event id collided → deduped (no second email)
    expect(deps.notifications.inApp).toHaveLength(1)
    expect(mail.sent).toHaveLength(1)

    // The fake provider records both calls — that is the fake's naivety, NOT a double-charge. Real
    // provider idempotency is the host's job via the chargeKey it passed (both calls used 'order-9').
    expect(deps.payment.charges).toHaveLength(2)
  })

  it('unauthorized buyer (no order:create): rejected at the community auth gate → ZERO side effects across BOTH presets', async () => {
    // clean slate — nothing listed, so the zero asserts are unambiguous
    await expect(
      runMarketplacePurchase(deps, validPurchase({ buyerHeaders: bearer('tok-browser') })),
    ).rejects.toBeInstanceOf(PermissionDeniedError)

    // fail-closed: no charge, no ledger split, no seller notice, no audit — the gate ran before any of it
    expect(deps.payment.charges).toHaveLength(0)
    const journal = await deps.ledger.journal()
    expect(journal.count).toBe(0)
    expect(deps.notifications.inApp).toHaveLength(0)
    expect(mail.sent).toHaveLength(0)
    const page = await listAudit(deps.auditDb, { entityType: 'order', action: 'order.settle' })
    expect(page.items).toHaveLength(0)
  })
})
