import { describe, expect, it } from 'vitest'
import {
  CREDIT_UNIT_PRICE_CENTS,
  canAcceptConversion,
  creditPurchaseTotalCents,
  projectCreditBalance,
  sourceExpiryFromAcceptedAt,
  zipExpiryFromCompletedAt,
  isExpired,
  clampSignedUrlTtlSeconds,
  submissionIdempotencyKey,
  reversalIdempotencyKey,
  publicErrors,
  toOwnedConversionDto,
  type ConversionRecord,
} from '../src/index.js'

describe('product money contracts', () => {
  it('uses exact USD minor units and bigint credits', () => {
    expect(CREDIT_UNIT_PRICE_CENTS).toBe(500n)
    expect(creditPurchaseTotalCents(37)).toBe(18_500n)
    expect(() => creditPurchaseTotalCents(1.5)).toThrow(RangeError)
  })

  it('projects spendable and deficit without rewriting net history', () => {
    expect(projectCreditBalance(3n)).toEqual({ spendable: 3n, deficit: 0n })
    expect(projectCreditBalance(-2n)).toEqual({ spendable: 0n, deficit: 2n })
    expect(canAcceptConversion(1n)).toBe(true)
    expect(canAcceptConversion(-1n)).toBe(false)
  })
})

describe('stable identities and deadlines', () => {
  it('derives deterministic, namespace-separated keys', () => {
    expect(submissionIdempotencyKey('user:1', 'request/1')).toBe(submissionIdempotencyKey('user:1', 'request/1'))
    expect(reversalIdempotencyKey('stripe', 'refund', 're_1')).not.toBe(submissionIdempotencyKey('stripe', 're_1'))
  })

  it('uses exact 48-hour instants and clamps download TTL', () => {
    const accepted = new Date('2026-08-23T12:00:00.000Z')
    const deadline = new Date('2026-08-25T12:00:00.000Z')
    expect(sourceExpiryFromAcceptedAt(accepted)).toEqual(deadline)
    expect(zipExpiryFromCompletedAt(accepted)).toEqual(deadline)
    expect(isExpired(deadline, new Date(deadline.getTime() - 1))).toBe(false)
    expect(isExpired(deadline, deadline)).toBe(true)
    expect(clampSignedUrlTtlSeconds(3600, deadline, new Date(deadline.getTime() - 1500))).toBe(1)
  })
})

describe('public ownership boundary', () => {
  const record: ConversionRecord = {
    id: 'c1', ownerId: 'u1', sourceFilename: 'document.pdf', status: 'Complete',
    acceptedAt: new Date('2026-08-23T12:00:00Z'), sourceExpiresAt: new Date('2026-08-25T12:00:00Z'),
    completedAt: new Date('2026-08-23T12:10:00Z'), zipExpiresAt: new Date('2026-08-25T12:10:00Z'),
    failureCode: null, publicFailureMessage: null, sourceStorageKey: 'private/source', zipStorageKey: 'private/zip',
    debitLedgerEntryId: 'ledger-1', compensationLedgerEntryId: null,
  }

  it('does not expose ownership, storage, or ledger internals', () => {
    const dto = toOwnedConversionDto(record, 'u1', new Date('2026-08-24T00:00:00Z'))
    expect(dto.canDownload).toBe(true)
    expect(dto).not.toHaveProperty('ownerId')
    expect(dto).not.toHaveProperty('zipStorageKey')
    expect(dto).not.toHaveProperty('debitLedgerEntryId')
  })

  it('uses the same not-found error across the ownership boundary', () => {
    expect(() => toOwnedConversionDto(record, 'u2')).toThrow(publicErrors.notFound().message)
  })
})
