import { describe, expect, it } from 'vitest'

import {
  AuditIntegrityError,
  AuditStorageError,
  AuditTrail,
  InMemoryAuditStore,
  type AuditIntegritySigner,
  type AuditReceipt,
  type AuditStore,
} from '../src/audit.js'

const request = {
  requestId: 'req-123',
  callerSubject: 'owner-123',
  applicationCredentialId: 'credential-123',
  operation: 'project.deploy.start',
  targetRef: 'project-123',
  limits: { requestBytes: 128, rateLimit: 10, concurrencyLimit: 1 },
}

const fixedClock = () => new Date('2026-08-13T10:00:00.000Z')
const signer = hmacSigner('test-only-integrity-key')

describe('AuditTrail', () => {
  it('appends accepted, rejected, and terminal receipts into a hash chain', async () => {
    const trail = new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock })

    const accepted = await trail.recordAccepted(request)
    const rejected = await trail.recordRejected({ ...request, requestId: 'req-124' }, 'rate_limited')
    const completed = await trail.recordTerminal({
      ...request,
      operationId: 'op-123',
      state: 'completed',
    })

    expect(accepted).toMatchObject({
      version: 1,
      sequence: 1,
      previousHash: null,
      state: 'accepted',
    })
    expect(rejected).toMatchObject({
      sequence: 2,
      previousHash: accepted.hash,
      state: 'rejected',
      rejectionCode: 'rate_limited',
    })
    expect(completed).toMatchObject({
      sequence: 3,
      previousHash: rejected.hash,
      operationSequence: 1,
      operationPreviousHash: null,
      state: 'completed',
      operationId: 'op-123',
    })
    expect(completed.hash).toMatch(/^[a-f0-9]{64}$/)
  })

  it('rejects a sequential competing terminal receipt', async () => {
    const trail = new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock })
    await trail.recordTerminal({ ...request, operationId: 'op-123', state: 'completed' })

    await expect(trail.recordTerminal({
      ...request,
      requestId: 'req-124',
      operationId: 'op-123',
      state: 'failed',
      rejectionCode: 'service_unavailable',
    })).rejects.toBeInstanceOf(AuditIntegrityError)
    await expect(trail.getOperationReceipts('owner-123', 'op-123')).resolves.toHaveLength(1)
  })

  it('allows only one concurrent competing terminal receipt', async () => {
    const trail = new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock })
    const results = await Promise.allSettled([
      trail.recordTerminal({ ...request, operationId: 'op-123', state: 'completed' }),
      trail.recordTerminal({
        ...request,
        requestId: 'req-124',
        operationId: 'op-123',
        state: 'failed',
        rejectionCode: 'service_unavailable',
      }),
    ])

    expect(results.filter((result) => result.status === 'fulfilled')).toHaveLength(1)
    expect(results.filter((result) => result.status === 'rejected')).toHaveLength(1)
    const receipts = await trail.getOperationReceipts('owner-123', 'op-123')
    expect(receipts).toHaveLength(1)
    expect(receipts[0]?.state).toMatch(/^(completed|failed)$/)
  })

  it('rejects unsafe fields and invalid receipt transitions before storage', async () => {
    const trail = new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock })

    await expect(trail.recordAccepted({ ...request, targetRef: '/host/path' })).rejects.toThrow('targetRef')
    expect(() => trail.recordTerminal({
      ...request,
      operationId: 'op-123',
      state: 'completed',
      rejectionCode: 'service_unavailable',
    })).toThrow('Completed')
    expect(() => trail.recordTerminal({
      ...request,
      operationId: 'op-123',
      state: 'failed',
    })).toThrow('Failed')
  })

  it('detects tampered chain heads instead of extending them', async () => {
    const store = new TamperedHeadStore()
    const trail = new AuditTrail(store, signer, { now: fixedClock })

    await expect(trail.recordAccepted(request)).rejects.toBeInstanceOf(AuditIntegrityError)
    expect(store.appendCalled).toBe(false)
  })

  it('allows operation retrieval despite globally interleaved operations', async () => {
    const trail = new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock })
    await trail.recordTerminal({ ...request, operationId: 'op-123', state: 'completed' })
    await trail.recordTerminal({ ...request, requestId: 'req-124', operationId: 'op-124', state: 'completed' })

    const receipts = await trail.getOperationReceipts('owner-123', 'op-123')

    expect(receipts).toHaveLength(1)
    expect(receipts[0]).toMatchObject({ sequence: 1, operationSequence: 1, operationPreviousHash: null })
  })

  it('rejects operation-chain splices, repeats, reordering, roots, and scope mismatches', async () => {
    const source = new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock })
    const first = await source.recordTerminal({ ...request, operationId: 'op-123', state: 'completed' })
    await source.recordTerminal({ ...request, requestId: 'req-124', operationId: 'op-124', state: 'completed' })
    const foreign = await source.recordTerminal({
      ...request,
      requestId: 'req-127',
      callerSubject: 'other-owner',
      operationId: 'op-123',
      state: 'completed',
    })

    const badChains: readonly (readonly AuditReceipt[])[] = [
      [first, { ...first, operationPreviousHash: first.previousHash }],
      [first, first],
      [{ ...first, operationSequence: 2, operationPreviousHash: first.hash }],
      [foreign],
    ]

    for (const receipts of badChains) {
      await expect(new AuditTrail(new UnfilteredReceiptListStore(receipts), signer).getOperationReceipts('owner-123', 'op-123'))
        .rejects.toBeInstanceOf(AuditIntegrityError)
    }
  })

  it('rejects missing operation-chain metadata and an operation head outside scope', async () => {
    const source = new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock })
    const first = await source.recordTerminal({ ...request, operationId: 'op-123', state: 'completed' })

    const foreign = await source.recordTerminal({
      ...request,
      requestId: 'req-125',
      callerSubject: 'other-owner',
      operationId: 'op-123',
      state: 'completed',
    })

    await expect(new AuditTrail(new ReceiptListStore([{ ...first, operationSequence: undefined }]), signer)
      .getOperationReceipts('owner-123', 'op-123')).rejects.toBeInstanceOf(AuditIntegrityError)
    await expect(new AuditTrail(new WrongOperationHeadStore(foreign), signer, { now: fixedClock })
      .recordTerminal({ ...request, requestId: 'req-124', operationId: 'op-123', state: 'completed' }))
      .rejects.toBeInstanceOf(AuditIntegrityError)
  })

  it('rejects a ledger rewrite authenticated only with a recomputed plain digest', async () => {
    const forged = await new AuditTrail(new InMemoryAuditStore(), signer, { now: fixedClock }).recordTerminal({
      ...request,
      operationId: 'op-123',
      state: 'completed',
    })
    const rewritten = { ...forged, state: 'failed' as const, rejectionCode: 'service_unavailable' as const }
    rewritten.hash = plainDigest(JSON.stringify(rewritten))
    const trail = new AuditTrail(new ForgedReadStore(rewritten), signer, { now: fixedClock })

    await expect(trail.getOperationReceipts('owner-123', 'op-123')).rejects.toBeInstanceOf(AuditIntegrityError)
  })

  it('fails closed with the wrong integrity key', async () => {
    const store = new InMemoryAuditStore()
    await new AuditTrail(store, signer, { now: fixedClock }).recordTerminal({ ...request, operationId: 'op-123', state: 'completed' })
    const trail = new AuditTrail(store, hmacSigner('wrong-key'), { now: fixedClock })

    await expect(trail.getOperationReceipts('owner-123', 'op-123')).rejects.toBeInstanceOf(AuditIntegrityError)
  })

  it('fails closed after repeated concurrent append conflicts', async () => {
    const trail = new AuditTrail(new ConflictingStore(), signer, { now: fixedClock, maxAppendRetries: 2 })

    await expect(trail.recordAccepted(request)).rejects.toBeInstanceOf(AuditStorageError)
  })
})

class ReceiptListStore implements AuditStore {
  constructor(protected readonly receipts: readonly AuditReceipt[]) {}
  async readHead(): Promise<AuditReceipt | null> { return this.receipts.at(-1) ?? null }
  async readOperationHead(callerSubject: string, operationId: string): Promise<AuditReceipt | null> {
    return [...this.receipts].reverse().find((receipt) => (
      receipt.callerSubject === callerSubject && receipt.operationId === operationId
    )) ?? null
  }
  async compareAndAppend(
    _receipt: AuditReceipt,
    _expectedPreviousHash: string | null,
    _expectedOperationPreviousHash: string | null,
  ): Promise<'conflict'> { return 'conflict' }
  async getByOperation(callerSubject: string, operationId: string, limit: number): Promise<readonly AuditReceipt[]> {
    return this.receipts.filter((receipt) => (
      receipt.callerSubject === callerSubject && receipt.operationId === operationId
    )).slice(0, limit)
  }
}

class UnfilteredReceiptListStore extends ReceiptListStore {
  override async getByOperation(
    _callerSubject: string,
    _operationId: string,
    limit: number,
  ): Promise<readonly AuditReceipt[]> {
    return this.getAll().slice(0, limit)
  }

  private getAll(): readonly AuditReceipt[] {
    return this.receipts
  }
}

class WrongOperationHeadStore implements AuditStore {
  constructor(private readonly receipt: AuditReceipt) {}
  async readHead(): Promise<null> { return null }
  async readOperationHead(_callerSubject: string, _operationId: string): Promise<AuditReceipt> { return this.receipt }
  async compareAndAppend(
    _receipt: AuditReceipt,
    _expectedPreviousHash: string | null,
    _expectedOperationPreviousHash: string | null,
  ): Promise<'conflict'> { return 'conflict' }
  async getByOperation(_callerSubject: string, _operationId: string, _limit: number): Promise<readonly AuditReceipt[]> { return [] }
}

class ForgedReadStore implements AuditStore {
  constructor(private readonly receipt: AuditReceipt) {}
  async readHead(): Promise<AuditReceipt> { return this.receipt }
  async readOperationHead(_callerSubject: string, _operationId: string): Promise<AuditReceipt> { return this.receipt }
  async compareAndAppend(
    _receipt: AuditReceipt,
    _expectedPreviousHash: string | null,
    _expectedOperationPreviousHash: string | null,
  ): Promise<'conflict'> { return 'conflict' }
  async getByOperation(_callerSubject: string, _operationId: string, _limit: number): Promise<readonly AuditReceipt[]> {
    return [this.receipt]
  }
}

function hmacSigner(key: string): AuditIntegritySigner {
  return {
    sign: (payload) => plainDigest(`${key.length}:${key}:${payload}`),
    verify: (payload, signature) => signature === plainDigest(`${key.length}:${key}:${payload}`),
  }
}

function plainDigest(value: string): string {
  let state = 0x811c9dc5
  for (const byte of new TextEncoder().encode(value)) {
    state = Math.imul(state ^ byte, 0x01000193) >>> 0
  }
  return Array.from({ length: 8 }, (_, index) => (state ^ Math.imul(index + 1, 0x9e3779b1)) >>> 0)
    .map((word) => word.toString(16).padStart(8, '0'))
    .join('')
}

class TamperedHeadStore implements AuditStore {
  appendCalled = false

  async readHead(): Promise<AuditReceipt> {
    return {
      version: 1,
      sequence: 1,
      previousHash: null,
      hash: '0'.repeat(64),
      recordedAt: '2026-08-13T10:00:00.000Z',
      requestId: 'req-123',
      callerSubject: 'owner-123',
      applicationCredentialId: 'credential-123',
      operation: 'project.deploy.start',
      state: 'accepted',
    }
  }

  async readOperationHead(_callerSubject: string, _operationId: string): Promise<AuditReceipt> {
    return this.readHead()
  }

  async compareAndAppend(
    _receipt: AuditReceipt,
    _expectedPreviousHash: string | null,
    _expectedOperationPreviousHash: string | null,
  ): Promise<'appended'> {
    this.appendCalled = true
    return 'appended'
  }

  async getByOperation(_callerSubject: string, _operationId: string, _limit: number): Promise<readonly AuditReceipt[]> {
    return []
  }
}

class ConflictingStore implements AuditStore {
  async readHead(): Promise<null> {
    return null
  }

  async readOperationHead(_callerSubject: string, _operationId: string): Promise<null> {
    return null
  }

  async compareAndAppend(
    _receipt: AuditReceipt,
    _expectedPreviousHash: string | null,
    _expectedOperationPreviousHash: string | null,
  ): Promise<'conflict'> {
    return 'conflict'
  }

  async getByOperation(_callerSubject: string, _operationId: string, _limit: number): Promise<readonly AuditReceipt[]> {
    return []
  }
}
