/**
 * Blueprint composition proof — commerce-storefront (single-seller store).
 *
 * Proves the commerce-* domain modules COMPOSE into a real buy flow:
 * catalog → cart → inventory reserve → order → billing charge → mark paid → mail job.
 * Does NOT re-test individual module surfaces.
 */
import { PGlite } from '@electric-sql/pglite'
import { drizzle } from 'drizzle-orm/pglite'
import { beforeEach, describe, expect, it } from 'vitest'
import {
  buildChargeKey,
  isPaymentFailedError,
  PaymentFailedError,
} from '@platform-modules/commerce-checkout'
import { variant } from '@platform-modules/commerce-catalog'
import { isOversoldError } from '@platform-modules/commerce-inventory'
import { order } from '@platform-modules/commerce-orders'
import type { PaymentProvider } from '@platform-modules/billing'
import type { CheckoutSchema } from '@platform-modules/commerce-checkout'
import type { Transaction, Querier } from '@platform-modules/db'
import type { InventorySchema } from '@platform-modules/commerce-inventory'
import { createJobRegistry, type JobEnvelope } from '@platform-modules/jobs'
import { createCaptureProvider, type CaptureProvider } from '../src/blueprints/commerce/wiring/billing'
import { createStorefrontCart, type StorefrontCart } from '../src/blueprints/commerce-storefront/wiring/cart'
import { createStorefrontCatalog, type StorefrontCatalog } from '../src/blueprints/commerce-storefront/wiring/catalog'
import {
  createStorefrontCheckout,
  type StorefrontCheckout,
} from '../src/blueprints/commerce-storefront/wiring/checkout'
import {
  createFakeFulfillmentPorts,
  type FakeFulfillmentPorts,
} from '../src/blueprints/commerce-storefront/wiring/fulfillment'
import {
  createStorefrontInventory,
  type StorefrontInventory,
} from '../src/blueprints/commerce-storefront/wiring/inventory'
import { createStorefrontMailer, type StorefrontMailer } from '../src/blueprints/commerce-storefront/wiring/mail'
import { createStorefrontOrders, type StorefrontOrders } from '../src/blueprints/commerce-storefront/wiring/orders'
import { createStorefrontPromotions, type PromotionsDb } from '../src/blueprints/commerce-storefront/wiring/promotions'
import { createStorefrontReviews, type ReviewsDb } from '../src/blueprints/commerce-storefront/wiring/reviews'
import { createProductIndex } from '../src/blueprints/commerce-storefront/wiring/search'

const BUYER_ID = '33333333-3333-4333-8333-333333333333'
const UNIT_PRICE = 11_800n
const CURRENCY = 'ILS'

type InventoryTx = Transaction<InventorySchema>

function inventoryTx(tx: unknown): InventoryTx {
  return tx as InventoryTx
}

type StorefrontDb = StorefrontCatalog['db'] &
  StorefrontOrders['db'] &
  StorefrontInventory['db'] &
  FakeFulfillmentPorts['db']

type StorefrontDeps = {
  db: StorefrontDb
  catalog: StorefrontCatalog
  cart: StorefrontCart
  inventory: StorefrontInventory
  orders: StorefrontOrders
  checkout: StorefrontCheckout
  fulfillment: FakeFulfillmentPorts
  payment: CaptureProvider
  mailer: StorefrontMailer
}

type MailJobEnv = { mailer: StorefrontMailer }

function createDecliningProvider(): CaptureProvider {
  const provider: PaymentProvider = {
    provider: 'declining',
    emitsInvoiceOnCharge: false,
    async charge() {
      throw new PaymentFailedError('CARD_DECLINED')
    },
    async refund() {
      return {
        kind: 'refunded',
        refundKey: 'none',
        chargeKey: 'none',
        providerRef: 'none',
        amount: 0,
        currency: 'USD',
      }
    },
    async parseWebhook(): Promise<never> {
      throw new Error('parseWebhook is outside the commerce blueprint flow')
    },
  }
  return { provider, charges: [], refunds: [] }
}

function orderConfirmationSchema() {
  return {
    '~standard': {
      version: 1 as const,
      vendor: 'test',
      validate: (value: unknown) =>
        typeof value === 'object' &&
        value !== null &&
        'orderId' in value &&
        'email' in value
          ? { value: value as { orderId: string; email: string } }
          : { issues: [{ message: 'invalid order confirmation payload' }] },
    },
  }
}

async function createStorefrontDb(): Promise<StorefrontDb> {
  const client = new PGlite()
  return drizzle(client) as unknown as StorefrontDb
}

describe('blueprint: commerce-storefront — catalog → cart → inventory → order → charge → mail composes', () => {
  let deps: StorefrontDeps
  let enqueuedJobs: JobEnvelope<{ orderId: string; email: string }>[]

  beforeEach(async () => {
    const db = await createStorefrontDb()
    await import('@platform-modules/commerce-checkout').then((m) =>
      m.pushSchema(db as unknown as Querier<CheckoutSchema>),
    )
    const catalog = await createStorefrontCatalog(db)
    const orders = await createStorefrontOrders(db)
    const inventory = await createStorefrontInventory(db)
    const fulfillment = await createFakeFulfillmentPorts(db)
    const payment = createCaptureProvider()
    const checkout = createStorefrontCheckout(db, payment.provider, fulfillment)
    const cart = createStorefrontCart()
    const mailer = createStorefrontMailer()
    enqueuedJobs = []

    deps = {
      db,
      catalog,
      cart,
      inventory,
      orders,
      checkout,
      fulfillment,
      payment,
      mailer,
    }
  })

  async function seedProductWithVariant() {
    const product = await deps.catalog.upsertProduct({
      kind: 'physical',
      slug: 'mechanical-keyboard',
      title: 'Mechanical Keyboard',
      description: 'Hot-swappable switches.',
      status: 'active',
    })
    const [variantRow] = await deps.catalog.db
      .insert(variant)
      .values({ productId: product.id, sku: 'kbd-default', attributes: {} })
      .returning()
    const variantId = variantRow!.id
    await deps.catalog.setVariantPrices(variantId, [
      { currency: CURRENCY, amount: UNIT_PRICE, priceMode: 'inclusive' },
    ])
    await deps.db.transaction(async (tx) => {
      await deps.inventory.setInventory(inventoryTx(tx), { skuId: variantId, quantityTotal: 10 })
    })
    return { product, variantId }
  }

  it('main flow: product → cart → reserve → order → charge → paid → mail job enqueued', async () => {
    const { product, variantId } = await seedProductWithVariant()
    const idempotencyKey = crypto.randomUUID()

    const productBySlug = await deps.catalog.getProductBySlug('mechanical-keyboard', { audience: 'public' })
    expect(productBySlug?.slug).toBe('mechanical-keyboard')

    const updatedCart = await deps.cart.applyToCart('cart-1', {
      type: 'addLine',
      variantId,
      qty: 1,
      price: { amount: UNIT_PRICE, currency: CURRENCY, priceMode: 'inclusive' },
      vendorId: null,
    })
    expect(updatedCart.lines).toHaveLength(1)
    expect(updatedCart.lines[0]?.variantId).toBe(variantId)

    await deps.db.transaction(async (tx) => {
      await deps.inventory.reserve(inventoryTx(tx), {
        skuId: variantId,
        qty: 1,
        holderRef: idempotencyKey,
        now: new Date(),
      })
    })
    const availableAfterReserve = await deps.inventory.getAvailability(deps.db, variantId)
    expect(availableAfterReserve).toBe(9)

    const startResult = await deps.checkout.startCheckout({
      cart: updatedCart,
      buyerRef: { userId: BUYER_ID },
      priceMode: 'inclusive',
      currency: CURRENCY,
      buyerCountry: 'IL',
      idempotencyKey,
    })
    expect(startResult.orderId).toBeTruthy()
    expect(deps.payment.charges).toHaveLength(1)
    expect(deps.checkout.ledger.entries.length).toBeGreaterThan(0)

    const providerRef = `ref-${buildChargeKey(startResult.orderId)}`
    const paid = await deps.checkout.settleCheckout(startResult.orderId, providerRef)
    expect(paid.status).toBe('paid')

    const secondSettle = await deps.checkout.settleCheckout(startResult.orderId, providerRef)
    expect(secondSettle.status).toBe('paid')

    const paidOrder = await deps.orders.getOrderById(deps.db, startResult.orderId, { isAdmin: true })
    expect(paidOrder?.status).toBe('paid')

    const registry = createJobRegistry<MailJobEnv>()
    registry.register('order.confirmation', orderConfirmationSchema(), async (env, payload) => {
      const body = payload!
      await env.mailer.mail.send({
        from: 'store@test.local',
        to: body.email,
        subject: `Order ${body.orderId} confirmed`,
        text: 'Thanks for your order.',
      })
    })

    const envelope: JobEnvelope<{ orderId: string; email: string }> = {
      type: 'order.confirmation',
      payload: { orderId: startResult.orderId, email: 'buyer@test.local' },
    }
    enqueuedJobs.push(envelope)
    await registry.dispatch({ mailer: deps.mailer }, envelope)

    expect(deps.mailer.captured).toHaveLength(1)
    expect(deps.mailer.captured[0]?.subject).toContain(startResult.orderId)
    expect(enqueuedJobs).toHaveLength(1)

    const index = createProductIndex()
    index.add({
      id: product.id,
      title: product.title,
      description: product.description ?? '',
    })
    const searchResult = await index.search('Mechanical')
    expect(searchResult.groups[0]?.hits).toHaveLength(1)

    const reviews = await createStorefrontReviews(deps.db as unknown as ReviewsDb)
    const submitted = await reviews.submitReview(
      deps.db as unknown as ReviewsDb,
      {
        productId: product.id,
        userId: BUYER_ID,
        purchaseId: startResult.orderId,
        vendorId: null,
        rating: 5,
        body: 'Solid build quality.',
      },
      {
        verifiedPurchase: {
          hasPurchased: async (userId, productId) =>
            userId === BUYER_ID && productId === product.id,
        },
      },
    )
    expect(submitted.productId).toBe(product.id)
    expect(submitted.status).toBe('pending')

    const promotions = await createStorefrontPromotions(deps.db as unknown as PromotionsDb)
    await expect(
      promotions.getPromoByCode(deps.db as unknown as PromotionsDb, 'NONE', null),
    ).resolves.toBeNull()
  })

  it('fail-closed (INV-2b): payment failure leaves order `charging`, not paid, not rolled back', async () => {
    const decliningPayment = createDecliningProvider()
    const decliningCheckout = createStorefrontCheckout(
      deps.db,
      decliningPayment.provider,
      deps.fulfillment,
    )

    const { variantId } = await seedProductWithVariant()
    await deps.db.transaction(async (tx) => {
      await deps.inventory.setInventory(inventoryTx(tx), { skuId: variantId, quantityTotal: 2 })
    })
    const stockBefore = await deps.inventory.getAvailability(deps.db, variantId)
    expect(stockBefore).toBe(2)

    const idempotencyKey = crypto.randomUUID()
    const cart = await deps.cart.applyToCart('cart-decline', {
      type: 'addLine',
      variantId,
      qty: 1,
      price: { amount: UNIT_PRICE, currency: CURRENCY, priceMode: 'inclusive' },
      vendorId: null,
    })

    await expect(
      decliningCheckout.startCheckout({
        cart,
        buyerRef: { userId: BUYER_ID },
        priceMode: 'inclusive',
        currency: CURRENCY,
        buyerCountry: 'IL',
        idempotencyKey,
      }),
    ).rejects.toSatisfy(isPaymentFailedError)

    // INV-2b: order committed in `charging` state; payment failed outside tx — not paid, not rolled back
    const orderRows = await deps.db.select().from(order).execute()
    const theOrder = orderRows.find((row) => row.idempotencyKey === idempotencyKey)
    expect(theOrder).toBeDefined()
    expect(theOrder?.status).toBe('charging')
    expect(orderRows.some((row) => row.status === 'paid')).toBe(false)
    // reserve is a separate caller step — startCheckout does not touch inventory directly
    expect(await deps.inventory.getAvailability(deps.db, variantId)).toBe(stockBefore)
  })

  it('fail-closed: reserve when stock=0 rejects (OversoldError)', async () => {
    const { variantId } = await seedProductWithVariant()
    const orderId = crypto.randomUUID()

    await deps.db.transaction(async (tx) => {
      await deps.inventory.setInventory(inventoryTx(tx), { skuId: variantId, quantityTotal: 0 })
    })

    await expect(
      deps.db.transaction(async (tx) =>
        deps.inventory.reserve(inventoryTx(tx), {
          skuId: variantId,
          qty: 1,
          holderRef: orderId,
          now: new Date(),
        }),
      ),
    ).rejects.toSatisfy(isOversoldError)
  })
})
