import { describe, expect, it } from 'vitest'
import { listAudit } from './list-audit.js'
import { createTestDb, seedAudit } from './test-fixture.js'

describe('listAudit', () => {
  async function seedFixture() {
    const db = await createTestDb()

    const ids = {
      t1a: await seedAudit(db, {
        tenantId: 'tenant-1',
        actorId: 'actor-a',
        actorLabel: 'Alpha User',
        action: 'deal.update',
        entityType: 'deal',
        entityId: 'deal-1',
        createdAt: new Date('2026-06-16T10:00:00.000Z'),
      }),
      t1b: await seedAudit(db, {
        tenantId: 'tenant-1',
        actorId: 'actor-b',
        actorLabel: 'Beta User',
        action: 'deal.create',
        entityType: 'deal',
        entityId: 'deal-2',
        createdAt: new Date('2026-06-16T11:00:00.000Z'),
      }),
      t1c: await seedAudit(db, {
        tenantId: 'tenant-1',
        actorId: 'actor-a',
        actorLabel: 'Alpha User',
        action: 'user.login',
        entityType: 'user',
        entityId: 'user-1',
        createdAt: new Date('2026-06-16T12:00:00.000Z'),
      }),
      t2a: await seedAudit(db, {
        tenantId: 'tenant-2',
        actorId: 'actor-c',
        actorLabel: 'Gamma User',
        action: 'order.ship',
        entityType: 'order',
        entityId: 'order-9',
        createdAt: new Date('2026-06-16T13:00:00.000Z'),
      }),
      t1d: await seedAudit(db, {
        tenantId: 'tenant-1',
        actorId: 'actor-b',
        actorLabel: 'Beta User',
        action: 'deal.archive',
        entityType: 'deal',
        entityId: 'deal-3',
        createdAt: new Date('2026-06-16T14:00:00.000Z'),
      }),
    }

    return { db, ids }
  }

  it('returns rows in created_at DESC order', async () => {
    const { db } = await seedFixture()
    const result = await listAudit(db, { tenant: 'tenant-1' })

    expect(result.items.map((row) => row.action)).toEqual([
      'deal.archive',
      'user.login',
      'deal.create',
      'deal.update',
    ])
  })

  it('keyset pagination has no overlap, no gap, and ends with null cursor', async () => {
    const { db } = await seedFixture()

    const page1 = await listAudit(db, { tenant: 'tenant-1', limit: 2 })
    expect(page1.items).toHaveLength(2)
    expect(page1.nextCursor).toBeTypeOf('string')

    const page2 = await listAudit(db, {
      tenant: 'tenant-1',
      limit: 2,
      cursor: page1.nextCursor!,
    })
    expect(page2.items).toHaveLength(2)
    expect(page2.nextCursor).toBeNull()

    const allIds = [...page1.items, ...page2.items].map((row) => row.id)
    expect(new Set(allIds).size).toBe(allIds.length)

    const full = await listAudit(db, { tenant: 'tenant-1' })
    expect(allIds).toEqual(full.items.map((row) => row.id))
  })

  it('tenant filter narrows results', async () => {
    const { db } = await seedFixture()
    const result = await listAudit(db, { tenant: 'tenant-2' })

    expect(result.items).toHaveLength(1)
    expect(result.items[0]?.action).toBe('order.ship')
  })

  it('actor filter narrows results', async () => {
    const { db } = await seedFixture()
    const result = await listAudit(db, { tenant: 'tenant-1', actor: 'actor-a' })

    expect(result.items).toHaveLength(2)
    expect(result.items.every((row) => row.actorId === 'actor-a')).toBe(true)
  })

  it('entityType and entityId filters narrow results', async () => {
    const { db } = await seedFixture()
    const result = await listAudit(db, {
      tenant: 'tenant-1',
      entityType: 'deal',
      entityId: 'deal-2',
    })

    expect(result.items).toHaveLength(1)
    expect(result.items[0]?.action).toBe('deal.create')
  })

  it('action filter narrows results', async () => {
    const { db } = await seedFixture()
    const result = await listAudit(db, { tenant: 'tenant-1', action: 'user.login' })

    expect(result.items).toHaveLength(1)
    expect(result.items[0]?.entityType).toBe('user')
  })

  it('from and to date range filters narrow results', async () => {
    const { db } = await seedFixture()
    const result = await listAudit(db, {
      tenant: 'tenant-1',
      from: new Date('2026-06-16T10:30:00.000Z'),
      to: new Date('2026-06-16T12:30:00.000Z'),
    })

    expect(result.items.map((row) => row.action)).toEqual(['user.login', 'deal.create'])
  })

  it('q ILIKE filter matches action, entityType, or actorLabel substring', async () => {
    const { db } = await seedFixture()

    const byAction = await listAudit(db, { tenant: 'tenant-1', q: 'archive' })
    expect(byAction.items).toHaveLength(1)
    expect(byAction.items[0]?.action).toBe('deal.archive')

    const byEntity = await listAudit(db, { tenant: 'tenant-1', q: 'user' })
    expect(byEntity.items.some((row) => row.entityType === 'user')).toBe(true)

    const byLabel = await listAudit(db, { tenant: 'tenant-1', q: 'Beta' })
    expect(byLabel.items.every((row) => row.actorLabel?.includes('Beta'))).toBe(true)
  })

  it('empty result returns items:[] and nextCursor:null', async () => {
    const { db } = await seedFixture()
    const result = await listAudit(db, { tenant: 'tenant-1', action: 'missing.action' })

    expect(result).toEqual({ items: [], nextCursor: null })
  })

  // Regression (boundary spec amendment A): rows that share a millisecond must page
  // through the uuid tiebreaker with NO row dropped. The .000Z-aligned happy seed above
  // can never exercise this — the keyset boundary that previously dropped rows lands
  // inside a same-ms cluster.
  it('keyset does not drop rows that share a millisecond (uuid tiebreaker)', async () => {
    const db = await createTestDb()
    const sameMs = new Date('2026-06-16T10:00:00.123Z')
    for (let i = 0; i < 3; i++) {
      await seedAudit(db, {
        tenantId: 'tenant-1',
        action: `same.ms.${i}`,
        entityType: 'thing',
        entityId: `e-${i}`,
        createdAt: sameMs,
      })
    }

    const seen: string[] = []
    let cursor: string | null | undefined
    for (let page = 0; page < 5; page++) {
      const res = await listAudit(db, { tenant: 'tenant-1', limit: 1, cursor: cursor ?? undefined })
      seen.push(...res.items.map((r) => r.id))
      cursor = res.nextCursor
      if (cursor === null) break
    }

    const full = await listAudit(db, { tenant: 'tenant-1' })
    expect(seen).toHaveLength(3)
    expect(new Set(seen).size).toBe(3) // no overlap
    expect(seen).toEqual(full.items.map((r) => r.id)) // no gap, same order
  })

  it('clamps an unbounded limit and survives NaN without crashing', async () => {
    const { db } = await seedFixture()

    const huge = await listAudit(db, { tenant: 'tenant-1', limit: 1_000_000_000 })
    expect(huge.items.length).toBeLessThanOrEqual(200) // MAX_LIMIT, not 1e9

    const nan = await listAudit(db, { tenant: 'tenant-1', limit: Number.NaN })
    expect(nan.items.length).toBeGreaterThan(0) // NaN → default, not a TypeError crash
  })

  it('rejects an Invalid Date in from/to at the boundary', async () => {
    const { db } = await seedFixture()
    await expect(
      listAudit(db, { tenant: 'tenant-1', from: new Date('not-a-date') }),
    ).rejects.toThrow(RangeError)
    await expect(
      listAudit(db, { tenant: 'tenant-1', to: new Date('not-a-date') }),
    ).rejects.toThrow(RangeError)
  })
})
