import { PGlite } from '@electric-sql/pglite'
import { drizzle } from 'drizzle-orm/pglite'
import { beforeEach, describe, expect, it } from 'vitest'
import type { Querier } from '@platform-modules/db'
import {
  auditLog,
  auditSchema,
  listAudit,
  logAudit,
  type AuditSchema,
} from '@platform-modules/audit'

type AuditDb = Querier<AuditSchema>

/**
 * Inline pglite + the module's own DDL — the consumer must consume `@platform-modules/audit`
 * through its PUBLIC barrel only. The module ships an internal `test-fixture.ts`, but it is NOT
 * a published subpath (single `.` export), so a real consumer reconstructs the table from the
 * schema it owns. created_at is timestamptz(3) — the ms precision the keyset cursor round-trips.
 */
async function createAuditDb(): Promise<AuditDb> {
  const client = new PGlite()
  const db = drizzle(client, { schema: auditSchema }) as unknown as AuditDb

  await client.exec(`
    CREATE TABLE audit_log (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      actor_id text,
      actor_type text,
      actor_label text,
      action text NOT NULL,
      entity_type text NOT NULL,
      entity_id text NOT NULL,
      metadata jsonb,
      ip text,
      tenant_id text,
      created_at timestamptz(3) NOT NULL DEFAULT NOW()
    );
    CREATE INDEX audit_log_tenant_created_idx ON audit_log (tenant_id, created_at DESC);
    CREATE INDEX audit_log_tenant_entity_idx ON audit_log (tenant_id, entity_type, entity_id);
    CREATE INDEX audit_log_tenant_actor_idx ON audit_log (tenant_id, actor_id);
  `)

  return db
}

/**
 * Deterministic seed via the exported `auditLog` schema — the tx-atomic insert path the boundary
 * spec (amendment B) documents: a host that needs audit to commit/rollback with its business tx,
 * OR that needs a controlled timestamp, inserts the schema directly. `logAudit` deliberately
 * server-stamps `created_at` (NOW) and gives the caller NO timestamp control — forging an audit
 * timestamp would breach audit integrity — so ordering tests must seed timestamps this way.
 */
async function seedAt(db: AuditDb, createdAt: Date, fields: typeof auditLog.$inferInsert): Promise<void> {
  await db.insert(auditLog).values({ ...fields, createdAt })
}

describe('audit consumer fixture (Gate 3 — real writes/lists through the public barrel)', () => {
  let db: AuditDb

  beforeEach(async () => {
    db = await createAuditDb()
  })

  it('logAudit writes a human actor and a null-actor system event; both are retrievable', async () => {
    const human = await logAudit(db, {
      actorId: 'user-1',
      actorType: 'user',
      action: 'tenant.suspend',
      entityType: 'tenant',
      entityId: 'tenant-9',
      tenantId: 'tenant-9',
    })
    const system = await logAudit(db, {
      actorId: null,
      actorType: 'system',
      action: 'job.retention_sweep',
      entityType: 'tenant',
      entityId: 'tenant-9',
      tenantId: 'tenant-9',
    })

    expect(human).toEqual({ logged: true, id: expect.any(String) })
    expect(system).toEqual({ logged: true, id: expect.any(String) })

    const page = await listAudit(db, { tenant: 'tenant-9' })
    expect(page.items.map((r) => r.action).sort()).toEqual([
      'job.retention_sweep',
      'tenant.suspend',
    ])
    // the system event has no actor — the log still records it (actorId nullable, not dropped)
    const sweep = page.items.find((r) => r.action === 'job.retention_sweep')
    expect(sweep?.actorId).toBeNull()
    expect(sweep?.actorType).toBe('system')
  })

  it('filters by entityType and keyset-paginates without dropping or repeating a row', async () => {
    const base = Date.parse('2026-06-16T12:00:00.000Z')
    for (let i = 0; i < 5; i++) {
      await seedAt(db, new Date(base + i * 1000), {
        actorId: `user-${i}`,
        action: 'invoice.view',
        entityType: 'invoice',
        entityId: `inv-${i}`,
        tenantId: 't-1',
      })
    }
    // a different entityType + the newest row — the filter must exclude it from every page
    await seedAt(db, new Date(base + 9000), {
      actorId: 'user-x',
      action: 'tenant.update',
      entityType: 'tenant',
      entityId: 't-1',
      tenantId: 't-1',
    })

    const page1 = await listAudit(db, { tenant: 't-1', entityType: 'invoice', limit: 2 })
    expect(page1.items.map((r) => r.entityId)).toEqual(['inv-4', 'inv-3'])
    expect(page1.nextCursor).not.toBeNull()

    const page2 = await listAudit(db, {
      tenant: 't-1',
      entityType: 'invoice',
      limit: 2,
      cursor: page1.nextCursor ?? undefined,
    })
    expect(page2.items.map((r) => r.entityId)).toEqual(['inv-2', 'inv-1'])

    const page3 = await listAudit(db, {
      tenant: 't-1',
      entityType: 'invoice',
      limit: 2,
      cursor: page2.nextCursor ?? undefined,
    })
    expect(page3.items.map((r) => r.entityId)).toEqual(['inv-0'])
    expect(page3.nextCursor).toBeNull()

    // the filter never leaked the newer non-invoice row across any page, and no row repeated
    const allIds = [...page1.items, ...page2.items, ...page3.items].map((r) => r.entityId)
    expect(allIds).not.toContain('t-1')
    expect(new Set(allIds).size).toBe(5)
  })

  it('never throws when the underlying insert fails — compliance gap is signalled, not crashed', async () => {
    const brokenDb = {
      insert: () => ({
        values: () => ({
          returning: () => Promise.reject(new Error('connection terminated')),
        }),
      }),
    } as unknown as AuditDb

    const result = await logAudit(brokenDb, {
      actorId: 'user-1',
      action: 'tenant.delete',
      entityType: 'tenant',
      entityId: 'tenant-9',
    })

    expect(result.logged).toBe(false)
    expect(result.id).toBeUndefined()
    expect(result.error).toBeInstanceOf(Error)
  })
})
