import { sql } from 'drizzle-orm'
import { createPgliteClient } from '@platform-modules/db/pglite'
import { describe, expect, it } from 'vitest'
import {
  IdempotencyConflictError,
  isEntitlementDeniedError,
  isIdempotencyConflictError,
  isMeteringValidationError,
  isMeteringStorageError,
  isQuotaExhaustedError,
} from './errors.js'
import { pushSchema } from './migrate.js'
import { reserve } from './reserve.js'
import { meteringSchema } from './schema.js'
import type { EntitlementPolicy, SubjectRef } from './types.js'

const NOW = new Date('2026-07-15T12:00:00.000Z')
const SUBJECT: SubjectRef = { tenantId: 'tenant-a', account: 'account-a' }
const PERIOD = {
  id: 'period-a',
  startsAt: new Date('2026-07-01T00:00:00.000Z'),
  endsAt: new Date('2026-08-01T00:00:00.000Z'),
}

function policy(overrides: Partial<EntitlementPolicy> = {}): EntitlementPolicy {
  return {
    meter: 'api.calls',
    includedUnits: 5n,
    overage: 'deny',
    period: PERIOD,
    version: 'policy-a',
    ...overrides,
  }
}

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

function deps(db: Awaited<ReturnType<typeof freshDb>>, resolved: EntitlementPolicy | null = policy()) {
  return {
    db,
    policies: { resolve: async () => resolved },
    clock: () => NOW,
  }
}

function input(overrides: Partial<{
  subject: SubjectRef
  meter: string
  units: bigint
  idempotencyKey: string
  expiresAt: Date
}> = {}) {
  return {
    subject: SUBJECT,
    meter: 'api.calls',
    units: 2n,
    idempotencyKey: 'request-a',
    expiresAt: new Date('2026-07-15T13:00:00.000Z'),
    ...overrides,
  }
}

function storedReservationRow() {
  return {
    id: 'reservation-a',
    tenant_id: SUBJECT.tenantId,
    idempotency_key: 'request-a',
    fingerprint: JSON.stringify([
      SUBJECT.tenantId,
      SUBJECT.account,
      'api.calls',
      '2',
      '2026-07-15T13:00:00.000Z',
      'period-a',
    ]),
    account: SUBJECT.account,
    meter: 'api.calls',
    period_id: 'period-a',
    period_starts_at: PERIOD.startsAt,
    period_ends_at: PERIOD.endsAt,
    reserved_units: '2',
    committed_units: null,
    status: 'reserved',
    applied_included_units: '5',
    applied_hard_limit: null,
    applied_overage: 'deny',
    policy_version: 'policy-a',
    expires_at: new Date('2026-07-15T13:00:00.000Z'),
  }
}

describe('reserve', () => {
  it('enforces includedUnits for deny policies even when hardLimit is present', async () => {
    const db = await freshDb()
    const reserveDeps = deps(db, policy({ hardLimit: 20n }))

    await reserve(reserveDeps, input({ units: 5n }))

    let thrown: unknown
    try {
      await reserve(reserveDeps, input({ units: 1n, idempotencyKey: 'request-b' }))
    } catch (error) {
      thrown = error
    }

    expect(isQuotaExhaustedError(thrown)).toBe(true)
    expect(thrown).toMatchObject({ requested: 1n, includedRemaining: 0n, capacityRemaining: 0n })
  })

  it('uses hardLimit as the ceiling for allow policies', async () => {
    const db = await freshDb()
    const reserveDeps = deps(db, policy({ overage: 'allow', hardLimit: 7n }))

    await reserve(reserveDeps, input({ units: 5n }))
    await reserve(reserveDeps, input({ units: 2n, idempotencyKey: 'request-b' }))

    await expect(
      reserve(reserveDeps, input({ units: 1n, idempotencyKey: 'request-c' })),
    ).rejects.toSatisfy((error) => {
      return isQuotaExhaustedError(error) &&
        error.requested === 1n &&
        error.includedRemaining === 0n &&
        error.capacityRemaining === 0n
    })
  })

  it('admits any in-bound quantity for allow policies without hardLimit', async () => {
    const db = await freshDb()
    const reservation = await reserve(
      deps(db, policy({ overage: 'allow' })),
      input({ units: 999999999999999999999n }),
    )

    expect(reservation.reservedUnits).toBe(999999999999999999999n)
    expect(reservation.appliedPolicy).toEqual({
      includedUnits: 5n,
      overage: 'allow',
      version: 'policy-a',
    })
  })

  it('rejects null policy and non-positive quantities with typed errors', async () => {
    const deniedDb = await freshDb()
    await expect(
      reserve(deps(deniedDb, null), input()),
    ).rejects.toSatisfy(isEntitlementDeniedError)

    const invalidDb = await freshDb()
    const reserveDeps = deps(invalidDb)
    for (const units of [0n, -1n]) {
      await expect(
        reserve(reserveDeps, input({ units, idempotencyKey: `invalid-${units}` })),
      ).rejects.toSatisfy(isMeteringValidationError)
    }
    await expect(
      reserve(reserveDeps, input({ subject: undefined as unknown as SubjectRef })),
    ).rejects.toSatisfy(isMeteringValidationError)
  })

  it('replays the same tenant-scoped key and rejects a changed fingerprint', async () => {
    const db = await freshDb()
    const reserveDeps = deps(db)
    const first = await reserve(reserveDeps, input({ units: 2n }))
    const replay = await reserve(reserveDeps, input({ units: 2n }))

    expect(replay).toEqual(first)

    await expect(
      reserve(reserveDeps, input({ units: 3n })),
    ).rejects.toSatisfy(isIdempotencyConflictError)

    const otherTenant = await reserve(
      reserveDeps,
      input({
        subject: { tenantId: 'tenant-b', account: 'account-a' },
        idempotencyKey: 'request-a',
      }),
    )
    expect(otherTenant.id).not.toBe(first.id)
  })

  it('stores period and applied-policy snapshots on the reservation', async () => {
    const db = await freshDb()
    const applied = policy({
      includedUnits: 10n,
      hardLimit: 20n,
      overage: 'allow',
      version: 'policy-v2',
    })
    const reservation = await reserve(deps(db, applied), input({ units: 3n }))

    expect(reservation.period).toEqual(applied.period)
    expect(reservation.appliedPolicy).toEqual({
      includedUnits: 10n,
      hardLimit: 20n,
      overage: 'allow',
      version: 'policy-v2',
    })

    const result = await db.execute(sql`
      SELECT period_id, period_starts_at, period_ends_at,
             applied_included_units, applied_hard_limit, applied_overage, policy_version
      FROM metering_reservation
      WHERE id = ${reservation.id}
    `)
    const rows = (Array.isArray(result) ? result : result.rows) as Array<Record<string, unknown>>
    expect(rows[0]).toMatchObject({
      period_id: 'period-a',
      applied_included_units: '10',
      applied_hard_limit: '20',
      applied_overage: 'allow',
      policy_version: 'policy-v2',
    })
  })

  it('keeps counter position and reservation insertion atomic', async () => {
    const db = await freshDb()
    const reserveDeps = deps(db)
    const reservation = await reserve(reserveDeps, input({ units: 2n }))

    const result = await db.execute(sql`
      SELECT committed, reserved
      FROM metering_counter
      WHERE tenant_id = ${SUBJECT.tenantId}
        AND account = ${SUBJECT.account}
        AND meter = 'api.calls'
        AND period_id = 'period-a'
    `)
    const rows = (Array.isArray(result) ? result : result.rows) as Array<Record<string, unknown>>
    expect(rows[0]).toEqual({ committed: '0', reserved: '2' })
    expect(reservation.status).toBe('reserved')
  })

  it('exposes typed domain error instances without leaking storage failures', async () => {
    const db = await freshDb()
    const reserveDeps = deps(db, null)

    await expect(reserve(reserveDeps, input())).rejects.toSatisfy(isEntitlementDeniedError)
    await expect(reserve(reserveDeps, input({ idempotencyKey: 'request-b' }))).rejects.toSatisfy(
      isEntitlementDeniedError,
    )
    expect(isIdempotencyConflictError(new Error('different error'))).toBe(false)
  })

  it('wraps injected clock failures as typed storage errors', async () => {
    const db = await freshDb()
    const clockFailure = new Error('clock unavailable')

    await expect(
      reserve(
        { ...deps(db), clock: () => {
          throw clockFailure
        } },
        input(),
      ),
    ).rejects.toSatisfy((error) => {
      return isMeteringStorageError(error) && error.operation === 'reserve.clock' && error.cause === clockFailure
    })
  })

  it('returns one reservation when a concurrent same-key insert loses the unique race', async () => {
    let transactionCount = 0
    let readyTransactions = 0
    let releaseBarrier!: () => void
    const barrier = new Promise<void>((resolveBarrier) => {
      releaseBarrier = resolveBarrier
    })
    const row = storedReservationRow()
    const fakeDb = {
      transaction: async <T>(
        callback: (tx: { execute: (query: unknown) => Promise<unknown> }) => Promise<T>,
      ): Promise<T> => {
        const transactionId = transactionCount++
        let step = 0
        const tx = {
          execute: async (_query: unknown): Promise<unknown> => {
            const currentStep = step++
            if (currentStep === 0) return []
            if (currentStep === 1) return [{ committed: 0n, reserved: 0n }]
            if (currentStep === 2) {
              readyTransactions += 1
              if (readyTransactions === 2) releaseBarrier()
              await barrier
              return []
            }
            if (currentStep === 3) return transactionId === 0 ? [{ id: row.id }] : []
            if (currentStep === 4) return transactionId === 0 ? [] : [row]
            if (currentStep === 5) return [row]
            throw new Error(`unexpected transaction step: ${currentStep}`)
          },
        }
        return callback(tx)
      },
    }

    const reserveDeps = deps(fakeDb as Awaited<ReturnType<typeof freshDb>>)
    const [first, second] = await Promise.all([
      reserve(reserveDeps, input()),
      reserve(reserveDeps, input()),
    ])

    expect(second).toEqual(first)
    expect(first.id).toBe(row.id)
  })
})
