import { describe, it, expect } from 'vitest'
import { buildAuditEvent, audit } from '../src/audit/log.js'
import { assertZeroPayload, ZeroPayloadViolation } from '../src/audit/redact.js'
import type { AuditEvent, AuditSink } from '../src/types.js'
import { principal, fakeAuditSink } from './_helpers.js'

const NOW = 1_700_000_000

describe('audit log (INV10)', () => {
  it('builds a well-formed attach allow event and appends it', async () => {
    const sink = fakeAuditSink()
    const e = buildAuditEvent({
      action: 'attach',
      principal: principal('acct-A'),
      hostId: 'host-1',
      sessionId: null,
      jti: 'jti-1',
      outcome: 'allow',
      reason: 'ok',
      remoteAddrHash: 'hash',
      now: NOW,
    })
    await audit(sink, e)
    expect(sink.events).toHaveLength(1)
    expect(sink.events[0]!.action).toBe('attach')
    expect(sink.events[0]!.accountId).toBe('acct-A')
  })

  it('records a deny event with a reason', () => {
    const e = buildAuditEvent({
      action: 'cross-tenant-attempt',
      principal: null,
      hostId: 'host-B',
      sessionId: null,
      jti: null,
      outcome: 'deny',
      reason: 'cross_tenant',
      remoteAddrHash: 'hash',
      now: NOW,
    })
    expect(e.outcome).toBe('deny')
    expect(e.reason).toBe('cross_tenant')
    expect(e.principalId).toBe('')
  })

  it('throws (zero-payload guard) when a field contains ESC bytes', () => {
    const e: AuditEvent = {
      ts: new Date(NOW * 1000).toISOString(),
      action: 'attach',
      principalId: 'p',
      accountId: 'a',
      hostId: null,
      sessionId: null,
      jti: null,
      outcome: 'allow',
      reason: 'output:' + String.fromCharCode(0x1b) + '[31mred',
      remoteAddrHash: 'h',
    }
    expect(() => assertZeroPayload(e)).toThrow(ZeroPayloadViolation)
  })

  it('throws when a field exceeds the metadata length cap', () => {
    const e: AuditEvent = {
      ts: new Date(NOW * 1000).toISOString(),
      action: 'attach',
      principalId: 'p',
      accountId: 'a',
      hostId: null,
      sessionId: null,
      jti: null,
      outcome: 'allow',
      reason: 'x'.repeat(300),
      remoteAddrHash: 'h',
    }
    expect(() => assertZeroPayload(e)).toThrow(ZeroPayloadViolation)
  })

  it('audit() refuses to append a payload-bearing event', async () => {
    const bad: AuditEvent = {
      ts: new Date(NOW * 1000).toISOString(),
      action: 'attach',
      principalId: 'p',
      accountId: 'a',
      hostId: ']0;title',
      sessionId: null,
      jti: null,
      outcome: 'allow',
      reason: 'ok',
      remoteAddrHash: 'h',
    }
    const sink: AuditSink = { append: async () => undefined }
    await expect(audit(sink, bad)).rejects.toThrow(ZeroPayloadViolation)
  })
})
