import { describe, expect, it, vi } from 'vitest'
import { eq } from 'drizzle-orm'
import {
  ApiKeyExpiredError,
  ApiKeyLastUsedAtUpdateError,
  ApiKeyOwnerMismatchError,
  ApiKeyRevokedError,
  apiKeys,
  issueApiKey,
  verifyApiKey,
} from './index.js'
import { createTestDb, getApiKeyRecord } from './test-fixture.js'

describe('auth/api-keys', () => {
  it('issues a prefix.secret key, stores only the hashed secret, and verifies owner + scopes', async () => {
    const db = await createTestDb()
    const issued = await issueApiKey(db, {
      owner: 'site_123',
      issuedBy: { owner: 'site_123' },
      scopes: ['plugin.read', 'plugin.write'],
    })

    expect(issued.id).toMatch(
      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
    )

    const [prefix, secret] = issued.key.split('.')
    expect(prefix).toBeTruthy()
    expect(secret).toBeTruthy()

    const row = await getApiKeyRecord(db, issued.id)
    expect(row).toBeDefined()
    expect(row?.prefix).toBe(prefix)
    expect(row?.secretHash).not.toBe(secret)
    expect(row?.revokedAt).toBeNull()
    expect(row?.expiresAt).toBeNull()

    await expect(verifyApiKey(db, issued.key)).resolves.toEqual({
      owner: 'site_123',
      scopes: ['plugin.read', 'plugin.write'],
    })
  })

  it('returns null for a wrong secret', async () => {
    const db = await createTestDb()
    const issued = await issueApiKey(db, {
      owner: 'site_123',
      issuedBy: { owner: 'site_123' },
      scopes: ['plugin.read'],
    })
    const [prefix] = issued.key.split('.')

    await expect(verifyApiKey(db, `${prefix}.wrong-secret`)).resolves.toBeNull()
  })

  it('returns null for an unknown prefix', async () => {
    const db = await createTestDb()
    const issued = await issueApiKey(db, {
      owner: 'site_123',
      issuedBy: { owner: 'site_123' },
      scopes: ['plugin.read'],
    })
    const [, secret] = issued.key.split('.')

    await expect(verifyApiKey(db, `unknown-prefix.${secret}`)).resolves.toBeNull()
  })

  it('preserves scopes exactly as issued', async () => {
    const db = await createTestDb()
    const scopes = ['plugin.read', 'reports.export', 'reports.export']
    const issued = await issueApiKey(db, {
      owner: 'site_456',
      issuedBy: { owner: 'site_456' },
      scopes,
    })

    await expect(verifyApiKey(db, issued.key)).resolves.toEqual({
      owner: 'site_456',
      scopes,
    })
  })

  it('rejects issuing a key for an owner different from the authenticated issuer', async () => {
    const db = await createTestDb()

    await expect(
      issueApiKey(db, {
        owner: 'site_456',
        issuedBy: { owner: 'site_123' },
        scopes: ['plugin.read'],
      }),
    ).rejects.toBeInstanceOf(ApiKeyOwnerMismatchError)
  })

  it('rejects revoked keys with a typed error', async () => {
    const db = await createTestDb()
    const issued = await issueApiKey(db, {
      owner: 'site_123',
      issuedBy: { owner: 'site_123' },
      scopes: ['plugin.read'],
    })

    await db.update(apiKeys).set({ revokedAt: new Date() }).where(eq(apiKeys.id, issued.id))

    await expect(verifyApiKey(db, issued.key)).rejects.toBeInstanceOf(ApiKeyRevokedError)
  })

  it('rejects expired keys with a typed error', async () => {
    const db = await createTestDb()
    const issued = await issueApiKey(db, {
      owner: 'site_123',
      issuedBy: { owner: 'site_123' },
      scopes: ['plugin.read'],
      expiresAt: new Date('2026-01-01T00:00:00.000Z'),
    })

    await expect(
      verifyApiKey(db, issued.key, { now: new Date('2026-01-02T00:00:00.000Z') }),
    ).rejects.toBeInstanceOf(ApiKeyExpiredError)
  })

  it('reports last-used update failures without failing authentication', async () => {
    const db = await createTestDb()
    const issued = await issueApiKey(db, {
      owner: 'site_123',
      issuedBy: { owner: 'site_123' },
      scopes: ['plugin.read'],
    })
    const onLastUsedAtUpdateError = vi.fn()
    const failingDb = new Proxy(db, {
      get(target, property, receiver) {
        if (property === 'update') {
          return () => {
            throw new Error('db unavailable')
          }
        }
        return Reflect.get(target, property, receiver)
      },
    }) as typeof db

    await expect(
      verifyApiKey(failingDb, issued.key, { onLastUsedAtUpdateError }),
    ).resolves.toEqual({
      owner: 'site_123',
      scopes: ['plugin.read'],
    })
    expect(onLastUsedAtUpdateError).toHaveBeenCalledWith(expect.any(ApiKeyLastUsedAtUpdateError))
  })
})
