import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createFakeSupabase } from './stubs/fake-supabase'
import { ACTOR, APP_ORIGIN, USER_ID, billingFor, seedCatalog, seedPermits, seedTrip } from './stubs/purchase-fixtures'
import type { Purchase, PurchaseItem, RoutingProduct, TripCartItem } from '@/types/db'

/**
 * Tests A, B, D, E (docs/TASKS-2026-09-29-SYNCHRON-PURCHASING.md §13):
 * cart → purchase with a price snapshot, idempotent on the cart.
 */

const db = createFakeSupabase()
const mocks = vi.hoisted(() => ({
  logTripEvent: vi.fn(async () => {}),
  syncTripToHha: vi.fn(async () => ({ ok: true })),
  logOperationEvent: vi.fn(async () => {}),
  requestRouteCheckoutCalls: 0,
}))
vi.mock('@/lib/supabase/admin', () => ({ createAdminClient: () => db }))
vi.mock('@/lib/audit', () => ({ logTripEvent: mocks.logTripEvent }))
vi.mock('@/lib/integrations/hha-trip-sync', () => ({ syncTripToHha: mocks.syncTripToHha }))
vi.mock('@/lib/observability/operation-events', () => ({ logOperationEvent: mocks.logOperationEvent }))
// Count adapter calls without replacing the mock adapter's behaviour (Test E).
vi.mock('@/lib/integrations/synchron-payments', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@/lib/integrations/synchron-payments')>()
  return {
    ...actual,
    getSynchronPayments: (opts: Parameters<typeof actual.getSynchronPayments>[0]) => {
      const adapter = actual.getSynchronPayments(opts)
      const original = adapter.requestRouteCheckout.bind(adapter)
      return { ...adapter, requestRouteCheckout: (req: Parameters<typeof original>[0]) => { mocks.requestRouteCheckoutCalls += 1; return original(req) } }
    },
  }
})

const { addCartItem, createPurchaseFromCart, getOrCreateOpenCart, priceCart, loadPurchase, requestCheckout } = await import('@/lib/data/purchases')
const { resolveRoutingProduct } = await import('@/lib/data/routing-products')

beforeEach(() => {
  db.reset()
  seedCatalog(db)
  mocks.requestRouteCheckoutCalls = 0
  vi.stubEnv('SYNCHRON_PAYMENTS_MODE', 'mock')
})
afterEach(() => { vi.clearAllMocks(); vi.unstubAllEnvs() })

async function addLine(trip: ReturnType<typeof seedTrip>, permitId: string, productCode: 'express_route' | 'extended_route') {
  const resolved = await resolveRoutingProduct(productCode)
  if (!resolved.ok) throw new Error(`catalog: ${resolved.reason}`)
  const res = await addCartItem({ trip, ...ACTOR, permitId, productCode, product: resolved.product })
  if (!res.ok) throw new Error(res.error)
  return res.cart
}

describe('Test A — one express route becomes one pending purchase', () => {
  it('snapshots 199 cents on the item and the purchase total', async () => {
    const trip = seedTrip(db)
    const [tx] = seedPermits(db, trip.id, ['TX'])
    await addLine(trip, tx.id as string, 'express_route')

    const result = await createPurchaseFromCart({ trip, ...ACTOR, billing: billingFor(trip), companyId: null })
    expect(result.ok).toBe(true)
    if (!result.ok) return
    expect(result.reused).toBe(false)

    const purchases = db.table('purchases') as unknown as Purchase[]
    expect(purchases).toHaveLength(1)
    expect(purchases[0].purchase_status).toBe('pending_checkout')
    expect(purchases[0].total_amount_cents).toBe(199)
    expect(purchases[0].purchase_reference).toMatch(/^hhp_[A-Z2-9]{20}$/)

    const items = db.table('purchase_items') as unknown as PurchaseItem[]
    expect(items).toHaveLength(1)
    expect(items[0].purchase_id).toBe(purchases[0].id)
    expect(items[0].unit_price_cents).toBe(199)
    expect(items[0].line_total_cents).toBe(199)
    expect(items[0].route_type).toBe('express')
    expect(items[0].fulfillment_status).toBe('awaiting_payment')
    expect(items[0].hha_permit_id).toBe(tx.id)

    // The cart is consumed and points at the purchase.
    const cart = db.table('trip_carts')[0]
    expect(cart.status).toBe('checked_out')
    expect(cart.purchase_id).toBe(purchases[0].id)
  })
})

describe('Test B — mixed cart', () => {
  it('2× express + 1× extended totals 1398 across three items on one purchase', async () => {
    const trip = seedTrip(db)
    const [tx, ok, la] = seedPermits(db, trip.id, ['TX', 'OK', 'LA'])
    await addLine(trip, tx.id as string, 'express_route')
    await addLine(trip, ok.id as string, 'express_route')
    const priced = await addLine(trip, la.id as string, 'extended_route')
    expect(priced.total_cents).toBe(1398)

    const result = await createPurchaseFromCart({ trip, ...ACTOR, billing: billingFor(trip), companyId: null })
    expect(result.ok).toBe(true)
    if (!result.ok) return
    expect(result.bundle.purchase.total_amount_cents).toBe(1398)
    expect(result.bundle.purchase.subtotal_cents).toBe(1398)
    expect(db.table('purchases')).toHaveLength(1)
    const items = db.table('purchase_items') as unknown as PurchaseItem[]
    expect(items).toHaveLength(3)
    expect(items.every((i) => i.purchase_id === result.bundle.purchase.id)).toBe(true)
    expect(items.map((i) => i.line_total_cents).sort((a, b) => a - b)).toEqual([199, 199, 1000])
  })
})

describe('Test D — price change after purchase', () => {
  it('keeps the snapshot on the existing item; a new cart line prices at the new catalog price', async () => {
    const trip = seedTrip(db)
    const [tx, ok] = seedPermits(db, trip.id, ['TX', 'OK'])
    await addLine(trip, tx.id as string, 'express_route')
    const first = await createPurchaseFromCart({ trip, ...ACTOR, billing: billingFor(trip), companyId: null })
    expect(first.ok).toBe(true)
    if (!first.ok) return

    const product = db.table('routing_products').find((p) => p.product_code === 'express_route') as unknown as RoutingProduct
    product.selling_price_cents = 299
    product.product_version = 2

    const reloaded = await loadPurchase(first.bundle.purchase.id)
    expect(reloaded?.items[0].unit_price_cents).toBe(199)
    expect(reloaded?.items[0].product_version).toBe(1)
    expect(reloaded?.purchase.total_amount_cents).toBe(199)

    const cart = await addLine(trip, ok.id as string, 'express_route')
    expect(cart.items).toHaveLength(1)
    expect(cart.items[0].unit_price_cents).toBe(299)
    expect(cart.items[0].product_version).toBe(2)
    expect(cart.total_cents).toBe(299)
  })
})

describe('Test E — idempotent purchase and checkout', () => {
  it('a double submit of the same open cart yields one purchase; the second call is reused', async () => {
    const trip = seedTrip(db)
    const [tx] = seedPermits(db, trip.id, ['TX'])
    await addLine(trip, tx.id as string, 'express_route')
    const cart = await getOrCreateOpenCart(trip.id, USER_ID)
    expect((await priceCart(cart)).items).toHaveLength(1)

    // Two requests race on the same open cart (a double click): both read
    // the open cart and compute the same idempotency key `cart:<id>`.
    const params = { trip, ...ACTOR, billing: billingFor(trip), companyId: null }
    const [first, second] = await Promise.all([createPurchaseFromCart(params), createPurchaseFromCart(params)])
    expect(first.ok && second.ok).toBe(true)
    if (!first.ok || !second.ok) return
    expect(first.bundle.purchase.id).toBe(second.bundle.purchase.id)
    expect([first.reused, second.reused].sort()).toEqual([false, true])
    expect(db.table('purchases')).toHaveLength(1)
    expect(db.table('purchases')[0].idempotency_key).toBe(`cart:${cart.id}`)
    expect(db.table('purchase_items')).toHaveLength(1)
    expect((db.table('trip_cart_items') as unknown as TripCartItem[])[0].cart_id).toBe(cart.id)

    // Checkout twice: the adapter is asked once, the second call reuses the session.
    const checkout = { billing: billingFor(trip), actorUserId: USER_ID, actorLabel: ACTOR.actorLabel, appOrigin: APP_ORIGIN, scenario: 'checkout_created' as const }
    const c1 = await requestCheckout({ bundle: first.bundle, ...checkout })
    expect(c1).toMatchObject({ ok: true, reused: false })
    const bundle = await loadPurchase(first.bundle.purchase.id)
    expect(bundle?.purchase.purchase_status).toBe('pending_payment')
    const c2 = await requestCheckout({ bundle: bundle!, ...checkout })
    expect(c2).toMatchObject({ ok: true, reused: true })
    if (c1.ok && c2.ok) expect(c2.checkoutUrl).toBe(c1.checkoutUrl)
    expect(mocks.requestRouteCheckoutCalls).toBe(1)
    expect(db.table('payment_references')).toHaveLength(1)
  })

  it('a retry after the cart was consumed cannot create a second purchase', async () => {
    const trip = seedTrip(db)
    const [tx] = seedPermits(db, trip.id, ['TX'])
    await addLine(trip, tx.id as string, 'express_route')
    const params = { trip, ...ACTOR, billing: billingFor(trip), companyId: null }
    const first = await createPurchaseFromCart(params)
    expect(first.ok).toBe(true)
    const again = await createPurchaseFromCart(params)
    // The open cart was checked out; a fresh one is empty, so nothing is created.
    expect(again).toEqual({ ok: false, status: 400, error: 'Your cart is empty.' })
    expect(db.table('purchases')).toHaveLength(1)
  })
})
