import { sql } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { createPgliteClient } from '../../db/src/postgres/pglite.js'
import { commit } from './commit.js'
import {
  isMeteringValidationError,
  isQuotaExhaustedError,
  isReservationConflictError,
  isReservationExpiredError,
  isReservationNotFoundError,
} from './errors.js'
import { pushSchema } from './migrate.js'
import { meteringSchema } from './schema.js'

const PERIOD_START = new Date('2026-07-01T00:00:00.000Z')
const PERIOD_END = new Date('2026-08-01T00:00:00.000Z')
const COMMITTED_AT = new Date('2026-07-15T12:00:00.000Z')

type Seed = {
  reservationId?: string
  status?: 'reserved' | 'committed' | 'released' | 'expired'
  reservedUnits?: bigint
  committedUnits?: bigint | null
  counterCommitted?: bigint
  counterReserved?: bigint
  includedUnits?: bigint
  hardLimit?: bigint | null
  overage?: 'deny' | 'allow'
}

async function freshDb(seed: Seed = {}) {
  const db = createPgliteClient({ schema: meteringSchema })
  await pushSchema(db)

  const reservationId = seed.reservationId ?? 'reservation-1'
  const reservedUnits = seed.reservedUnits ?? 5n
  await db.execute(sql`
    INSERT INTO metering_counter (tenant_id, account, meter, period_id, committed, reserved)
    VALUES ('tenant-1', 'account-1', 'api.calls', 'period-1',
      ${seed.counterCommitted ?? 0n}, ${seed.counterReserved ?? reservedUnits})
  `)
  await db.execute(sql`
    INSERT INTO metering_reservation (
      id, tenant_id, idempotency_key, fingerprint, account, meter, period_id,
      period_starts_at, period_ends_at, reserved_units, committed_units, status,
      applied_included_units, applied_hard_limit, applied_overage, policy_version,
      expires_at
    ) VALUES (
      ${reservationId}, 'tenant-1', ${`key-${reservationId}`}, 'fingerprint-1',
      'account-1', 'api.calls', 'period-1',
      ${PERIOD_START.toISOString()}::timestamptz,
      ${PERIOD_END.toISOString()}::timestamptz,
      ${reservedUnits}, ${seed.committedUnits ?? null}, ${seed.status ?? 'reserved'},
      ${seed.includedUnits ?? 10n}, ${seed.hardLimit ?? null},
      ${seed.overage ?? 'deny'}, 'policy-1',
      '2026-07-16T12:00:00.000Z'::timestamptz
    )
  `)

  return { db, reservationId }
}

function rows(result: unknown): Array<Record<string, unknown>> {
  return (Array.isArray(result) ? result : (result as { rows?: Array<Record<string, unknown>> }).rows) ?? []
}

function bigintify(row: Record<string, unknown> | undefined, fields: string[]) {
  if (!row) return row
  return Object.fromEntries(
    Object.entries(row).map(([key, value]) =>
      fields.includes(key) && value !== null && value !== undefined
        ? [key, BigInt(String(value))]
        : [key, value],
    ),
  )
}

async function readState(
  db: Awaited<ReturnType<typeof freshDb>>['db'],
  reservationId: string,
) {
  const reservation = rows(await db.execute(sql`
    SELECT status, reserved_units, committed_units
    FROM metering_reservation WHERE id = ${reservationId}
  `))[0]
  const counter = rows(await db.execute(sql`
    SELECT committed, reserved
    FROM metering_counter
    WHERE tenant_id = 'tenant-1' AND account = 'account-1'
      AND meter = 'api.calls' AND period_id = 'period-1'
  `))[0]
  const events = rows(await db.execute(sql`
    SELECT units, kind, rule_version
    FROM metering_usage_event WHERE reservation_id = ${reservationId}
  `)).map((row) => bigintify(row, ['units']))

  return {
    reservation: bigintify(reservation, ['reserved_units', 'committed_units']),
    counter: bigintify(counter, ['committed', 'reserved']),
    events,
  }
}

describe('commit', () => {
  it('commits actual units, releases the difference, and appends one event', async () => {
    const { db, reservationId } = await freshDb()

    const result = await commit({ db, clock: () => COMMITTED_AT }, reservationId, 3n)

    expect(result.committedUnits).toBe(3n)
    expect(result.releasedUnits).toBe(2n)
    expect(result.reservation).toMatchObject({
      id: reservationId,
      status: 'committed',
      reservedUnits: 5n,
      committedUnits: 3n,
    })
    await expect(readState(db, reservationId)).resolves.toMatchObject({
      reservation: { status: 'committed', reserved_units: 5n, committed_units: 3n },
      counter: { committed: 3n, reserved: 0n },
      events: [{ units: 3n, kind: 'commit', rule_version: null }],
    })
  })

  it('replays same actual units idempotently without another event', async () => {
    const { db, reservationId } = await freshDb()

    const first = await commit({ db, clock: () => COMMITTED_AT }, reservationId, 3n)
    const second = await commit({ db, clock: () => new Date('2030-01-01T00:00:00.000Z') }, reservationId, 3n)

    expect(second).toEqual(first)
    expect((await readState(db, reservationId)).events).toHaveLength(1)
  })

  it('rejects a different actual-unit replay as a typed conflict', async () => {
    const { db, reservationId } = await freshDb()
    await commit({ db, clock: () => COMMITTED_AT }, reservationId, 3n)

    await expect(commit({ db }, reservationId, 4n)).rejects.toSatisfy((error) =>
      isReservationConflictError(error),
    )
  })

  it('checks over-commit capacity against the pinned policy and leaves state unchanged on failure', async () => {
    const { db, reservationId } = await freshDb({
      counterCommitted: 5n,
      counterReserved: 5n,
      includedUnits: 5n,
      hardLimit: 10n,
      overage: 'allow',
    })
    const before = await readState(db, reservationId)

    await expect(commit({ db, clock: () => COMMITTED_AT }, reservationId, 6n)).rejects.toSatisfy(
      (error) => isQuotaExhaustedError(error),
    )

    expect(await readState(db, reservationId)).toEqual(before)
  })

  it('uses the pinned hard limit for a successful over-commit', async () => {
    const { db, reservationId } = await freshDb({
      counterCommitted: 5n,
      counterReserved: 5n,
      includedUnits: 5n,
      hardLimit: 12n,
      overage: 'allow',
    })

    const result = await commit({ db, clock: () => COMMITTED_AT }, reservationId, 7n)

    expect(result).toMatchObject({ committedUnits: 7n, releasedUnits: 0n })
    expect((await readState(db, reservationId)).counter).toMatchObject({
      committed: 12n,
      reserved: 0n,
    })
  })

  it('accepts zero as a full hold release and still appends one commit event', async () => {
    const { db, reservationId } = await freshDb()

    const result = await commit({ db, clock: () => COMMITTED_AT }, reservationId, 0n)

    expect(result).toMatchObject({ committedUnits: 0n, releasedUnits: 5n })
    expect((await readState(db, reservationId)).events).toHaveLength(1)
  })

  it('returns typed errors for invalid, unknown, expired, and cross-terminal commits', async () => {
    const { db, reservationId } = await freshDb()

    await expect(commit({ db }, reservationId, -1n)).rejects.toSatisfy((error) =>
      isMeteringValidationError(error),
    )
    await expect(commit({ db }, 'missing', 1n)).rejects.toSatisfy((error) =>
      isReservationNotFoundError(error),
    )

    const expired = await freshDb({ reservationId: 'expired-1', status: 'expired' })
    await expect(commit({ db: expired.db }, expired.reservationId, 1n)).rejects.toSatisfy(
      (error) => isReservationExpiredError(error),
    )

    const released = await freshDb({ reservationId: 'released-1', status: 'released' })
    await expect(commit({ db: released.db }, released.reservationId, 1n)).rejects.toSatisfy(
      (error) => isReservationConflictError(error),
    )
  })
})
