/**
 * Hard-floor data-integrity conformance suite (Gate 4 formally N/A — floors are NOT skipped).
 *
 * Real-PG concurrency floors (do NOT duplicate embedded-postgres here):
 * - secaudit-promotions-over-redemption-atomic → record-redemption.test.ts
 *   ('deterministic interleave on maxUses=1: one winner, loser → global quota exhausted')
 * - secaudit-promotions-per-user-cap → record-redemption.test.ts
 *   ('per-user cap=1: second redemption for same user throws per_user')
 *   ('per-user cap=2: third redemption throws per_user (not a unique-constraint cap-of-1)')
 */
import { describe, expect, it } from 'vitest'
import { createPgliteClient } from '@platform-modules/db/pglite'
import { applyPromo } from './apply.js'
import { isPromoValidationError, PromoValidationError } from './errors.js'
import { pushSchema } from './migrate.js'
import { promo, promotionsSchema } from './schema.js'
import type { DiscountableCart, DiscountLine, DiscountResult, Promo, PromoContext } from './types.js'
import { validatePromo } from './validate.js'

const PROMO_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
const NOW = new Date('2026-06-20T12:00:00.000Z')

function basePromo(overrides: Partial<Promo> = {}): Promo {
  return {
    id: PROMO_ID,
    code: 'SAVE10',
    kind: 'percentage',
    valueBps: 1000,
    scope: { kind: 'all' },
    eligibility: {},
    funder: 'platform',
    active: true,
    vendorId: null,
    ...overrides,
  }
}

function line(
  lineId: string,
  unitPrice: bigint,
  qty: number,
  extras: Partial<DiscountLine> = {},
): DiscountLine {
  return { lineId, unitPrice, qty, vendorId: null, ...extras }
}

function cart(lines: DiscountLine[]): DiscountableCart {
  return { currency: 'USD', lines }
}

function scopedSubtotal(lines: DiscountLine[]): bigint {
  return lines.reduce((sum, entry) => sum + entry.unitPrice * BigInt(entry.qty), 0n)
}

function assertMoneyInvariants(result: DiscountResult, scopedValue: bigint): void {
  const sum = result.perLine.reduce((acc, entry) => acc + entry.amount, 0n)
  expect(sum).toBe(result.total)
  for (const { amount } of result.perLine) {
    expect(amount >= 0n).toBe(true)
  }
  expect(result.total <= scopedValue).toBe(true)
}

async function freshDb() {
  const db = createPgliteClient({ schema: promotionsSchema })
  await pushSchema(db)
  return db
}

describe('secaudit-promotions-* conformance', () => {
  it('secaudit-promotions-discount-sum-exact', () => {
    const cases: Array<{ promo: Promo; lines: DiscountLine[] }> = [
      {
        promo: basePromo({ valueBps: 1000 }),
        lines: [line('line-c', 333n, 1), line('line-a', 333n, 1), line('line-b', 333n, 1)],
      },
      {
        promo: basePromo({
          kind: 'fixed',
          valueBps: undefined,
          valueAmount: 7n,
          currency: 'USD',
        }),
        lines: [line('line-a', 20n, 1), line('line-b', 50n, 1), line('line-c', 30n, 1)],
      },
      {
        promo: basePromo({
          kind: 'bogo',
          valueBps: undefined,
          bogo: { buyQty: 2, getQty: 1 },
        }),
        lines: [
          line('expensive', 1000n, 2, { productId: 'p1' }),
          line('cheap', 100n, 4, { productId: 'p2' }),
        ],
      },
    ]

    for (const { promo: p, lines: cartLines } of cases) {
      const scoped = scopedSubtotal(cartLines)
      const result = applyPromo(p, cart(cartLines))
      const sum = result.perLine.reduce((acc, entry) => acc + entry.amount, 0n)
      expect(sum).toBe(result.total)
      assertMoneyInvariants(result, scoped)
    }
  })

  it('secaudit-promotions-no-overdiscount', () => {
    const cases: Array<{ promo: Promo; lines: DiscountLine[] }> = [
      {
        promo: basePromo({ valueBps: 1000 }),
        lines: [line('line-1', 5000n, 1)],
      },
      {
        promo: basePromo({
          kind: 'fixed',
          valueBps: undefined,
          valueAmount: 5000n,
          currency: 'USD',
        }),
        lines: [line('line-1', 3000n, 1)],
      },
      {
        promo: basePromo({
          kind: 'bogo',
          valueBps: undefined,
          bogo: { buyQty: 1, getQty: 1 },
        }),
        lines: [line('line-a', 200n, 2), line('line-b', 100n, 2)],
      },
    ]

    for (const { promo: p, lines: cartLines } of cases) {
      const scoped = scopedSubtotal(cartLines)
      const result = applyPromo(p, cart(cartLines))
      assertMoneyInvariants(result, scoped)
    }
  })

  it('secaudit-promotions-reject-is-data', () => {
    const ctx: PromoContext = {
      now: NOW,
      userId: null,
      cartSubtotal: 10_000n,
      cartCurrency: 'USD',
      lines: [line('line-1', 10_000n, 1)],
    }

    const rejections: Array<{ promo: Promo; reason: string }> = [
      { promo: basePromo({ active: false }), reason: 'inactive' },
      {
        promo: basePromo({ startsAt: new Date('2026-06-21T00:00:00.000Z') }),
        reason: 'not_started',
      },
      {
        promo: basePromo({ endsAt: new Date('2026-06-19T00:00:00.000Z') }),
        reason: 'expired',
      },
      {
        promo: basePromo({ currency: 'USD', minOrderAmount: 5_000n }),
        reason: 'currency_mismatch',
      },
      {
        promo: basePromo({ currency: 'USD', minOrderAmount: 20_000n }),
        reason: 'min_order_not_met',
      },
      {
        promo: basePromo({ scope: { kind: 'products', ids: ['missing'] } }),
        reason: 'out_of_scope',
      },
      {
        promo: basePromo({ eligibility: { firstPurchaseOnly: true } }),
        reason: 'not_first_purchase',
      },
      {
        promo: basePromo({ eligibility: { membersOnly: true } }),
        reason: 'not_member',
      },
      {
        promo: basePromo({ eligibility: { allowlistOnly: true } }),
        reason: 'not_on_allowlist',
      },
      {
        promo: basePromo({ maxUses: 5 }),
        reason: 'quota_exhausted',
      },
      {
        promo: basePromo({ perUserCap: 2 }),
        reason: 'per_user_cap_reached',
      },
    ]

    for (const { promo: p, reason } of rejections) {
      const result = validatePromo(p, {
        ...ctx,
        cartCurrency: reason === 'currency_mismatch' ? 'EUR' : ctx.cartCurrency,
        cartSubtotal: reason === 'min_order_not_met' ? 10_000n : ctx.cartSubtotal,
        isFirstPurchase: reason === 'not_first_purchase' ? false : undefined,
        isMember: reason === 'not_member' ? false : undefined,
        isAllowlisted: reason === 'not_on_allowlist' ? false : undefined,
        globalUses: reason === 'quota_exhausted' ? 5 : undefined,
        userRedemptionCount: reason === 'per_user_cap_reached' ? 2 : undefined,
      })
      expect(result.ok).toBe(false)
      if (!result.ok) {
        expect(result.reason).toBe(reason)
      }
    }

    expect(() => validatePromo(basePromo({ valueBps: 12_000 }), ctx)).toThrow(PromoValidationError)
    expect(() =>
      validatePromo(
        basePromo({
          kind: 'fixed',
          valueBps: undefined,
          valueAmount: 500n,
        }),
        ctx,
      ),
    ).toThrow(PromoValidationError)

    try {
      validatePromo(basePromo({ valueBps: 12_000 }), ctx)
      expect.unreachable('expected throw')
    } catch (e) {
      expect(isPromoValidationError(e)).toBe(true)
    }

    const plain = validatePromo(basePromo({ active: false }), ctx)
    expect(plain).toEqual({ ok: false, reason: 'inactive' })
  })

  it('secaudit-promotions-check-constraints', async () => {
    const db = await freshDb()
    const base = {
      id: PROMO_ID,
      code: 'CHK',
      kind: 'percentage' as const,
      scope: { kind: 'all' as const },
      eligibility: {},
      funder: 'platform' as const,
      vendorId: null,
      active: true,
    }

    await expect(
      db.insert(promo).values({ ...base, valueBps: 20_000 }),
    ).rejects.toThrow()

    await expect(
      db.insert(promo).values({ ...base, id: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', valueBps: 0 }),
    ).rejects.toThrow()

    await expect(
      db.insert(promo).values({
        ...base,
        id: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
        valueBps: 1000,
        maxUses: 0,
      }),
    ).rejects.toThrow()

    await expect(
      db.insert(promo).values({
        ...base,
        id: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
        valueBps: 1000,
        perUserCap: 0,
      }),
    ).rejects.toThrow()

    await expect(
      db.insert(promo).values({
        ...base,
        id: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
        kind: 'fixed',
        valueBps: null,
        valueAmount: -100n,
        currency: 'USD',
      }),
    ).rejects.toThrow()
  })
})
