import { describe, expect, it } from 'vitest'
import { CartValidationError, MixedCurrencyError } from './errors.js'
import { cartReduce, groupByVendor } from './reduce.js'
import type { Cart, PriceSnapshot } from './types.js'

const usdPrice = (amount: bigint): PriceSnapshot => ({
  amount,
  currency: 'USD',
  priceMode: 'inclusive',
})

const eurPrice = (amount: bigint): PriceSnapshot => ({
  amount,
  currency: 'EUR',
  priceMode: 'inclusive',
})

function emptyCart(id = 'cart-1'): Cart {
  return { id, currency: '', lines: [], subtotal: 0n }
}

describe('cartReduce', () => {
  it('addLine creates a line and bigint subtotal', () => {
    const next = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 2,
      price: usdPrice(1500n),
    })

    expect(next.lines).toHaveLength(1)
    expect(next.lines[0]?.qty).toBe(2)
    expect(next.lines[0]?.price.amount).toBe(1500n)
    expect(next.currency).toBe('USD')
    expect(next.subtotal).toBe(3000n)
    expect(typeof next.subtotal).toBe('bigint')
  })

  it('addLine on an existing variant increments qty and refreshes the snapshot', () => {
    const first = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })
    const lineId = first.lines[0]!.lineId

    const second = cartReduce(first, {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 2,
      price: usdPrice(1200n),
    })

    expect(second.lines).toHaveLength(1)
    expect(second.lines[0]?.lineId).toBe(lineId)
    expect(second.lines[0]?.qty).toBe(3)
    expect(second.lines[0]?.price.amount).toBe(1200n)
    expect(second.subtotal).toBe(3600n)
  })

  it('addLine subtotal stays exact above 2^53 (bigint money, never float — C1)', () => {
    // Odd integer > 2^53: doubles CANNOT represent it, so a Number()-arithmetic
    // path silently lands on a neighbouring even value. Asserting the EXACT bigint
    // (not just typeof) catches a float-corrupted subtotal that coerces back to bigint.
    const huge = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-huge',
      qty: 1,
      price: usdPrice(9_007_199_254_740_993n), // 2^53 + 1
    })
    expect(huge.subtotal).toBe(9_007_199_254_740_993n)
    expect(typeof huge.subtotal).toBe('bigint')

    // Also exercise the `× BigInt(qty)` multiply path with an odd >2^53 product.
    const multiplied = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-mult',
      qty: 3,
      price: usdPrice(3_000_000_000_000_001n),
    })
    expect(multiplied.subtotal).toBe(9_000_000_000_000_003n)
  })

  it('setQty updates subtotal', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })
    const lineId = cart.lines[0]!.lineId

    const next = cartReduce(cart, { type: 'setQty', lineId, qty: 4 })
    expect(next.lines[0]?.qty).toBe(4)
    expect(next.subtotal).toBe(4000n)
  })

  it('setQty with qty=0 throws CartValidationError for qty', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })

    expect(() =>
      cartReduce(cart, { type: 'setQty', lineId: cart.lines[0]!.lineId, qty: 0 }),
    ).toThrow(CartValidationError)
    try {
      cartReduce(cart, { type: 'setQty', lineId: cart.lines[0]!.lineId, qty: 0 })
    } catch (error) {
      expect(error).toBeInstanceOf(CartValidationError)
      expect((error as CartValidationError).field).toBe('qty')
    }
  })

  it('setQty on unknown lineId throws CartValidationError for lineId', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })

    expect(() => cartReduce(cart, { type: 'setQty', lineId: 'missing', qty: 2 })).toThrow(
      CartValidationError,
    )
    try {
      cartReduce(cart, { type: 'setQty', lineId: 'missing', qty: 2 })
    } catch (error) {
      expect((error as CartValidationError).field).toBe('lineId')
    }
  })

  it('removeLine on unknown lineId is an idempotent no-op', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })

    const next = cartReduce(cart, { type: 'removeLine', lineId: 'missing' })
    expect(next).toEqual(cart)
    expect(next).not.toBe(cart)
  })

  it('removeLine drops an existing line', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })
    const lineId = cart.lines[0]!.lineId

    const next = cartReduce(cart, { type: 'removeLine', lineId })
    expect(next.lines).toHaveLength(0)
    expect(next.subtotal).toBe(0n)
    expect(next.currency).toBe('')
  })

  it('clear empties lines and zeroes subtotal', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 2,
      price: usdPrice(1000n),
    })

    const next = cartReduce(cart, { type: 'clear' })
    expect(next.lines).toEqual([])
    expect(next.subtotal).toBe(0n)
    expect(next.currency).toBe('')
  })

  it('addLine with a second currency throws MixedCurrencyError', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })

    expect(() =>
      cartReduce(cart, {
        type: 'addLine',
        variantId: 'variant-b',
        qty: 1,
        price: eurPrice(1000n),
      }),
    ).toThrow(MixedCurrencyError)
  })

  it('never mutates the input cart', () => {
    const cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'variant-a',
      qty: 1,
      price: usdPrice(1000n),
    })
    const linesBefore = cart.lines.map((line) => ({
      ...line,
      price: { ...line.price },
    }))

    const next = cartReduce(cart, {
      type: 'addLine',
      variantId: 'variant-b',
      qty: 1,
      price: usdPrice(500n),
    })

    expect(next).not.toBe(cart)
    expect(cart.lines).toEqual(linesBefore)
    expect(cart.lines).toHaveLength(1)
    expect(next.lines).toHaveLength(2)
  })
})

describe('groupByVendor', () => {
  it('splits a two-vendor cart into stable first-appearance groups', () => {
    let cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'v1',
      qty: 1,
      price: usdPrice(1000n),
      vendorId: 'seller-a',
    })
    cart = cartReduce(cart, {
      type: 'addLine',
      variantId: 'v2',
      qty: 2,
      price: usdPrice(500n),
      vendorId: 'seller-b',
    })
    cart = cartReduce(cart, {
      type: 'addLine',
      variantId: 'v3',
      qty: 1,
      price: usdPrice(300n),
      vendorId: 'seller-a',
    })

    const groups = groupByVendor(cart)
    expect(groups).toHaveLength(2)
    expect(groups[0]?.vendorId).toBe('seller-a')
    expect(groups[0]?.lines).toHaveLength(2)
    expect(groups[0]?.subtotal).toBe(1300n)
    expect(groups[1]?.vendorId).toBe('seller-b')
    expect(groups[1]?.lines).toHaveLength(1)
    expect(groups[1]?.subtotal).toBe(1000n)
  })

  it('groups all-null vendor lines into a single group', () => {
    let cart = cartReduce(emptyCart(), {
      type: 'addLine',
      variantId: 'v1',
      qty: 1,
      price: usdPrice(1000n),
    })
    cart = cartReduce(cart, {
      type: 'addLine',
      variantId: 'v2',
      qty: 1,
      price: usdPrice(2000n),
    })

    const groups = groupByVendor(cart)
    expect(groups).toHaveLength(1)
    expect(groups[0]?.vendorId).toBeNull()
    expect(groups[0]?.lines).toHaveLength(2)
    expect(groups[0]?.subtotal).toBe(3000n)
  })
})
