import { describe, expect, it } from 'vitest'
import { assertOrderIntegrity, isOrderIntegrityError } from '@platform-modules/commerce-orders'
import { computeVendorSplits } from './splits.js'
import { isSplitIntegrityError } from './errors.js'

function sumSplits(splits: { amount: bigint }[]): bigint {
  let sum = 0n
  for (const split of splits) {
    sum += split.amount
  }
  return sum
}

describe('splits-single-vendor', () => {
  it('allocates commission to platform and net to vendor', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    const total = 10000n
    const splits = computeVendorSplits({
      lines: [{ vendorId, lineTotal: total }],
      subtotal: total,
      total,
      rates: new Map([[vendorId, 1000]]),
    })

    expect(splits).toHaveLength(2)
    expect(splits[0]).toEqual({ vendorId, amount: 9000n, funder: 'vendor' })
    expect(splits[1]).toEqual({ vendorId: null, amount: 1000n, funder: 'platform' })
    expect(sumSplits(splits)).toBe(total)
  })
})

describe('splits-multi-vendor-exact', () => {
  const v1 = '11111111-1111-1111-1111-111111111111'
  const v2 = '22222222-2222-2222-2222-222222222222'
  const v3 = '33333333-3333-3333-3333-333333333333'
  const subtotal = 100n
  const total = 103n

  it('sums to total exactly via largest-remainder', () => {
    const splits = computeVendorSplits({
      lines: [
        { vendorId: v1, lineTotal: 33n },
        { vendorId: v2, lineTotal: 33n },
        { vendorId: v3, lineTotal: 34n },
      ],
      subtotal,
      total,
      rates: new Map([
        [v1, 1000],
        [v2, 1000],
        [v3, 1000],
      ]),
    })

    expect(sumSplits(splits)).toBe(total)

    const vendorAmounts = new Map(
      splits.filter((s) => s.funder === 'vendor').map((s) => [s.vendorId, s.amount]),
    )
    expect(vendorAmounts.get(v1)).toBe(31n)
    expect(vendorAmounts.get(v2)).toBe(31n)
    expect(vendorAmounts.get(v3)).toBe(32n)

    const platform = splits.find((s) => s.funder === 'platform')
    expect(platform?.amount).toBe(9n)
  })

  it('naive per-bucket floor under-allocates (teeth)', () => {
    let naiveSum = 0n
    for (const lineTotal of [33n, 33n, 34n]) {
      naiveSum += (total * lineTotal) / subtotal
    }
    expect(naiveSum).toBeLessThan(total)
  })
})

describe('splits-mixed-platform-lines', () => {
  it('platform split includes own-line share plus commissions', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    const subtotal = 10000n
    const total = 10000n
    const splits = computeVendorSplits({
      lines: [
        { vendorId, lineTotal: 6000n },
        { vendorId: null, lineTotal: 4000n },
      ],
      subtotal,
      total,
      rates: new Map([[vendorId, 1000]]),
    })

    expect(sumSplits(splits)).toBe(total)
    expect(splits).toContainEqual({ vendorId, amount: 5400n, funder: 'vendor' })
    expect(splits).toContainEqual({ vendorId: null, amount: 4600n, funder: 'platform' })
  })
})

describe('splits-tax-in-total', () => {
  it('allocates across total not subtotal when tax is present', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    const subtotal = 10000n
    const total = 11000n
    const splits = computeVendorSplits({
      lines: [{ vendorId, lineTotal: subtotal }],
      subtotal,
      total,
      rates: new Map([[vendorId, 1000]]),
    })

    expect(sumSplits(splits)).toBe(total)
    expect(sumSplits(splits)).not.toBe(subtotal)
    expect(splits).toContainEqual({ vendorId, amount: 9900n, funder: 'vendor' })
    expect(splits).toContainEqual({ vendorId: null, amount: 1100n, funder: 'platform' })
  })
})

describe('splits-discount-in-total', () => {
  it('allocates across discounted total', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    const subtotal = 10000n
    const total = 9000n
    const splits = computeVendorSplits({
      lines: [{ vendorId, lineTotal: subtotal }],
      subtotal,
      total,
      rates: new Map([[vendorId, 1000]]),
    })

    expect(sumSplits(splits)).toBe(total)
    expect(splits).toContainEqual({ vendorId, amount: 8100n, funder: 'vendor' })
    expect(splits).toContainEqual({ vendorId: null, amount: 900n, funder: 'platform' })
  })
})

describe('splits-zero-rate', () => {
  it('vendor keeps full share and platform only gets own-line share', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    const subtotal = 10000n
    const total = 10000n
    const splits = computeVendorSplits({
      lines: [
        { vendorId, lineTotal: 7000n },
        { vendorId: null, lineTotal: 3000n },
      ],
      subtotal,
      total,
      rates: new Map([[vendorId, 0]]),
    })

    expect(sumSplits(splits)).toBe(total)
    expect(splits).toContainEqual({ vendorId, amount: 7000n, funder: 'vendor' })
    expect(splits).toContainEqual({ vendorId: null, amount: 3000n, funder: 'platform' })
  })
})

describe('splits-free-order', () => {
  it('returns pinned zero platform split', () => {
    const splits = computeVendorSplits({
      lines: [],
      subtotal: 0n,
      total: 0n,
      rates: new Map(),
    })

    expect(splits).toEqual([{ vendorId: null, amount: 0n, funder: 'platform' }])
    expect(sumSplits(splits)).toBe(0n)
  })

  it('throws when total is positive without lineable subtotal', () => {
    expect(() =>
      computeVendorSplits({
        lines: [],
        subtotal: 0n,
        total: 100n,
        rates: new Map(),
      }),
    ).toThrow('computeVendorSplits: total without lineable subtotal')
  })

  it('throws when line totals do not sum to subtotal (invariant breach)', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    expect(() =>
      computeVendorSplits({
        lines: [{ vendorId, lineTotal: 10n }],
        subtotal: 5n,
        total: 5n,
        rates: new Map([[vendorId, 1000]]),
      }),
    ).toThrow('computeVendorSplits: line totals do not sum to subtotal')
  })

  it('throws on a negative lineTotal (BigInt truncation would emit a negative split)', () => {
    const v1 = '11111111-1111-1111-1111-111111111111'
    const v2 = '22222222-2222-2222-2222-222222222222'
    // Old behavior: v1's floor share = 105×(−50)/100 truncates toward zero (−52),
    // emitting { v1, amount: -52n } — Σ still equals total, so integrity passes.
    expect(() =>
      computeVendorSplits({
        lines: [
          { vendorId: v1, lineTotal: -50n },
          { vendorId: v2, lineTotal: 150n },
        ],
        subtotal: 100n,
        total: 105n,
        rates: new Map([
          [v1, 0],
          [v2, 0],
        ]),
      }),
    ).toThrow('computeVendorSplits: negative lineTotal')
  })

  it('throws on a negative lineTotal even when it nets subtotal to 0n (short-circuit must not bypass the guard)', () => {
    const v1 = '11111111-1111-1111-1111-111111111111'
    const v2 = '22222222-2222-2222-2222-222222222222'
    expect(() =>
      computeVendorSplits({
        lines: [
          { vendorId: v1, lineTotal: 200n },
          { vendorId: v2, lineTotal: -200n },
        ],
        subtotal: 0n,
        total: 0n,
        rates: new Map(),
      }),
    ).toThrow('computeVendorSplits: negative lineTotal')
  })

  it('throws on a negative total/subtotal (precondition guard — no negative splits)', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    expect(() =>
      computeVendorSplits({
        lines: [{ vendorId, lineTotal: 100n }],
        subtotal: 100n,
        total: -50n,
        rates: new Map([[vendorId, 1000]]),
      }),
    ).toThrow('computeVendorSplits: negative total or subtotal')
    expect(() =>
      computeVendorSplits({
        lines: [{ vendorId, lineTotal: -100n }],
        subtotal: -100n,
        total: -100n,
        rates: new Map([[vendorId, 1000]]),
      }),
    ).toThrow('computeVendorSplits: negative total or subtotal')
  })
})

describe('splits-feeds-assertOrderIntegrity', () => {
  it('passes assertOrderIntegrity for computed splits', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    const subtotal = 10000n
    const tax = 1000n
    const discount = 500n
    const total = subtotal + tax - discount
    const lines = [{ vendorId, lineTotal: subtotal }]
    const splits = computeVendorSplits({
      lines,
      subtotal,
      total,
      rates: new Map([[vendorId, 1000]]),
    })

    expect(() =>
      assertOrderIntegrity({
        subtotal,
        tax,
        discount,
        total,
        lines,
        splits,
      }),
    ).not.toThrow()
  })

  it('throws OrderIntegrityError when a split amount is tampered', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    const subtotal = 10000n
    const total = 10000n
    const lines = [{ vendorId, lineTotal: subtotal }]
    const splits = computeVendorSplits({
      lines,
      subtotal,
      total,
      rates: new Map([[vendorId, 1000]]),
    })

    splits[0] = { ...splits[0]!, amount: splits[0]!.amount + 1n }

    try {
      assertOrderIntegrity({
        subtotal,
        tax: 0n,
        discount: 0n,
        total,
        lines,
        splits,
      })
      expect.unreachable('expected assertOrderIntegrity to throw')
    } catch (e) {
      expect(isOrderIntegrityError(e)).toBe(true)
      if (isOrderIntegrityError(e)) {
        expect(e.detail).toBe('splits')
      }
    }
  })

  it('rejects a negative line total with a typed SplitIntegrityError', () => {
    const vendorId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    try {
      computeVendorSplits({
        lines: [
          { vendorId, lineTotal: 200n },
          { vendorId: null, lineTotal: -100n },
        ],
        subtotal: 100n,
        total: 100n,
        rates: new Map(),
      })
      expect.unreachable('expected computeVendorSplits to throw')
    } catch (e) {
      expect(isSplitIntegrityError(e)).toBe(true)
      if (isSplitIntegrityError(e)) {
        expect(e.context.reason).toBe('negative lineTotal')
      }
    }
  })
})
