import { describe, expect, it } from 'vitest'
import { isPromoValidationError, PromoValidationError } from './errors.js'
import type { DiscountLine, Promo, PromoContext } from './types.js'
import { validatePromo } from './validate.js'

const PROMO_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-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 baseCtx(overrides: Partial<PromoContext> = {}): PromoContext {
  return {
    now: NOW,
    userId: null,
    cartSubtotal: 10_000n,
    cartCurrency: 'USD',
    lines: [{ lineId: 'line-1', unitPrice: 10_000n, qty: 1, vendorId: null }],
    ...overrides,
  }
}

describe('validatePromo', () => {
  it('rejects inactive promo', () => {
    const result = validatePromo(basePromo({ active: false }), baseCtx())
    expect(result).toEqual({ ok: false, reason: 'inactive' })
  })

  it('rejects not_started when now is before startsAt', () => {
    const result = validatePromo(
      basePromo({ startsAt: new Date('2026-06-21T00:00:00.000Z') }),
      baseCtx({ now: NOW }),
    )
    expect(result).toEqual({ ok: false, reason: 'not_started' })
  })

  it('rejects expired when now is after endsAt', () => {
    const result = validatePromo(
      basePromo({ endsAt: new Date('2026-06-19T00:00:00.000Z') }),
      baseCtx({ now: NOW }),
    )
    expect(result).toEqual({ ok: false, reason: 'expired' })
  })

  it('rejects currency_mismatch for percentage promo with currency and minOrderAmount', () => {
    const result = validatePromo(
      basePromo({
        currency: 'USD',
        minOrderAmount: 5_000n,
      }),
      baseCtx({ cartCurrency: 'EUR' }),
    )
    expect(result).toEqual({ ok: false, reason: 'currency_mismatch' })
  })

  it('rejects min_order_not_met when cart subtotal is below threshold', () => {
    const result = validatePromo(
      basePromo({
        currency: 'USD',
        minOrderAmount: 20_000n,
      }),
      baseCtx({ cartSubtotal: 10_000n }),
    )
    expect(result).toEqual({ ok: false, reason: 'min_order_not_met' })
  })

  it('rejects max_order_exceeded when cart subtotal is above maxOrderAmount', () => {
    const result = validatePromo(
      basePromo({
        currency: 'USD',
        minOrderAmount: 5_000n,
        maxOrderAmount: 9_000n,
      }),
      baseCtx({ cartSubtotal: 10_000n }),
    )
    expect(result).toEqual({ ok: false, reason: 'max_order_exceeded' })
  })

  it('rejects out_of_scope when no line matches products scope (fail-closed missing productId)', () => {
    const line: DiscountLine = {
      lineId: 'line-1',
      unitPrice: 10_000n,
      qty: 1,
      vendorId: null,
    }
    const result = validatePromo(
      basePromo({
        scope: { kind: 'products', ids: ['prod-1'] },
      }),
      baseCtx({ lines: [line] }),
    )
    expect(result).toEqual({ ok: false, reason: 'out_of_scope' })
  })

  it('rejects not_first_purchase when firstPurchaseOnly and ctx is not first purchase', () => {
    const result = validatePromo(
      basePromo({ eligibility: { firstPurchaseOnly: true } }),
      baseCtx({ isFirstPurchase: false }),
    )
    expect(result).toEqual({ ok: false, reason: 'not_first_purchase' })
  })

  it('rejects not_member when membersOnly and ctx is not a member', () => {
    const result = validatePromo(
      basePromo({ eligibility: { membersOnly: true } }),
      baseCtx({ isMember: false }),
    )
    expect(result).toEqual({ ok: false, reason: 'not_member' })
  })

  it('rejects not_on_allowlist when allowlistOnly and ctx is not allowlisted', () => {
    const result = validatePromo(
      basePromo({ eligibility: { allowlistOnly: true } }),
      baseCtx({ isAllowlisted: false }),
    )
    expect(result).toEqual({ ok: false, reason: 'not_on_allowlist' })
  })

  it('rejects quota_exhausted when advisory globalUses reaches maxUses', () => {
    const result = validatePromo(
      basePromo({ maxUses: 5 }),
      baseCtx({ globalUses: 5 }),
    )
    expect(result).toEqual({ ok: false, reason: 'quota_exhausted' })
  })

  it('rejects per_user_cap_reached when advisory userRedemptionCount reaches perUserCap', () => {
    const result = validatePromo(
      basePromo({ perUserCap: 2 }),
      baseCtx({ userRedemptionCount: 2 }),
    )
    expect(result).toEqual({ ok: false, reason: 'per_user_cap_reached' })
  })

  it('accepts a valid promo', () => {
    const promo = basePromo()
    const result = validatePromo(promo, baseCtx())
    expect(result).toEqual({ ok: true, promo })
  })

  it('throws PromoValidationError for percentage with bps above 10000', () => {
    expect(() =>
      validatePromo(basePromo({ valueBps: 12_000 }), baseCtx()),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for percentage with bps at or below zero', () => {
    expect(() => validatePromo(basePromo({ valueBps: 0 }), baseCtx())).toThrow(
      PromoValidationError,
    )
    expect(() => validatePromo(basePromo({ valueBps: -5 }), baseCtx())).toThrow(
      PromoValidationError,
    )
  })

  it('throws PromoValidationError for bogo with non-positive buyQty', () => {
    expect(() =>
      validatePromo(
        basePromo({
          kind: 'bogo',
          valueBps: undefined,
          bogo: { buyQty: 0, getQty: 1 },
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for bogo with non-positive getQty', () => {
    expect(() =>
      validatePromo(
        basePromo({
          kind: 'bogo',
          valueBps: undefined,
          bogo: { buyQty: 2, getQty: 0 },
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for fixed valueAmount without currency', () => {
    expect(() =>
      validatePromo(
        basePromo({
          kind: 'fixed',
          valueBps: undefined,
          valueAmount: 500n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for fixed valueAmount at or below zero', () => {
    const fixedBase = {
      kind: 'fixed' as const,
      valueBps: undefined,
      currency: 'USD' as const,
    }
    expect(() =>
      validatePromo(basePromo({ ...fixedBase, valueAmount: 0n }), baseCtx()),
    ).toThrow(PromoValidationError)
    expect(() =>
      validatePromo(basePromo({ ...fixedBase, valueAmount: -100n }), baseCtx()),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for percentage with minOrderAmount but no currency', () => {
    expect(() =>
      validatePromo(
        basePromo({
          minOrderAmount: 1_000n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for percentage with maxDiscountAmount but no currency', () => {
    expect(() =>
      validatePromo(
        basePromo({
          maxDiscountAmount: 500n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for percentage with maxDiscountAmount at or below zero', () => {
    expect(() =>
      validatePromo(
        basePromo({
          currency: 'USD',
          maxDiscountAmount: 0n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
    expect(() =>
      validatePromo(
        basePromo({
          currency: 'USD',
          maxDiscountAmount: -1n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for non-percentage promo with maxDiscountAmount', () => {
    expect(() =>
      validatePromo(
        basePromo({
          kind: 'fixed',
          valueBps: undefined,
          valueAmount: 500n,
          currency: 'USD',
          maxDiscountAmount: 200n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for maxOrderAmount at or below minOrderAmount', () => {
    expect(() =>
      validatePromo(
        basePromo({
          currency: 'USD',
          minOrderAmount: 5_000n,
          maxOrderAmount: 5_000n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
    expect(() =>
      validatePromo(
        basePromo({
          currency: 'USD',
          minOrderAmount: 5_000n,
          maxOrderAmount: 4_999n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for maxOrderAmount without currency', () => {
    expect(() =>
      validatePromo(
        basePromo({
          maxOrderAmount: 9_000n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('throws PromoValidationError for maxOrderAmount at or below zero', () => {
    expect(() =>
      validatePromo(
        basePromo({
          currency: 'USD',
          maxOrderAmount: 0n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
    expect(() =>
      validatePromo(
        basePromo({
          currency: 'USD',
          maxOrderAmount: -10n,
        }),
        baseCtx(),
      ),
    ).toThrow(PromoValidationError)
  })

  it('isPromoValidationError returns true on thrown PromoValidationError', () => {
    try {
      validatePromo(basePromo({ valueBps: 12_000 }), baseCtx())
      expect.unreachable('expected throw')
    } catch (e) {
      expect(isPromoValidationError(e)).toBe(true)
    }
  })

  it('isPromoValidationError returns false on plain Error', () => {
    expect(isPromoValidationError(new Error('nope'))).toBe(false)
  })
})
