import { generateKeyPairSync, sign } from 'node:crypto'
import { describe, expect, it } from 'vitest'

import {
  AuthenticationError,
  CloudflareAccessAssertionVerifier,
  DualFactorGatewayAuthenticator,
  RotatingApplicationCredentialVerifier,
  digestApplicationCredential,
  type EdgeIdentity,
} from '../src/auth.js'

const now = new Date('2026-08-13T12:00:00.000Z')
const keys = generateKeyPairSync('rsa', { modulusLength: 2_048 })
const publicKey = keys.publicKey.export({ format: 'jwk' })

function encode(value: unknown): string {
  return Buffer.from(JSON.stringify(value)).toString('base64url')
}

function assertion(overrides: Record<string, unknown> = {}): string {
  const header = encode({ alg: 'RS256', kid: 'primary' })
  const payload = encode({
    iss: 'https://team.cloudflareaccess.com',
    aud: 'gateway-audience',
    exp: Math.floor(now.getTime() / 1_000) + 60,
    iat: Math.floor(now.getTime() / 1_000) - 1,
    sub: 'owner-1',
    email: 'owner@example.test',
    sid: 'session-1',
    ...overrides,
  })
  const signature = sign('RSA-SHA256', Buffer.from(`${header}.${payload}`), keys.privateKey).toString('base64url')
  return `${header}.${payload}.${signature}`
}

function edgeVerifier() {
  return new CloudflareAccessAssertionVerifier({
    issuer: 'https://team.cloudflareaccess.com',
    audience: 'gateway-audience',
    resolveSigningKey: async (kid) => kid === 'primary' ? publicKey : undefined,
    now: () => now,
  })
}

describe('CloudflareAccessAssertionVerifier', () => {
  it('accepts a signed assertion with required claims', async () => {
    await expect(edgeVerifier().verify(assertion())).resolves.toEqual({
      subject: 'owner-1',
      email: 'owner@example.test',
      sessionId: 'session-1',
    })
  })

  it.each([
    ['forged signature', () => `${assertion()}x`],
    ['wrong issuer', () => assertion({ iss: 'https://forged.example' })],
    ['wrong audience', () => assertion({ aud: 'other-audience' })],
    ['expired assertion', () => assertion({ exp: Math.floor(now.getTime() / 1_000) - 31 })],
    ['missing session', () => assertion({ sid: '' })],
  ])('rejects %s', async (_name, makeAssertion) => {
    await expect(edgeVerifier().verify(makeAssertion())).rejects.toMatchObject({ code: 'unauthenticated' })
  })
})

describe('DualFactorGatewayAuthenticator', () => {
  async function authenticator(subject = 'owner-1') {
    return new DualFactorGatewayAuthenticator(
      edgeVerifier(),
      new RotatingApplicationCredentialVerifier([{
        id: 'credential-1',
        subject,
        secretDigest: await digestApplicationCredential('credential-secret'),
        expiresAt: new Date(now.getTime() + 60_000),
      }], () => now),
      () => now,
    )
  }

  it('requires independently valid edge and application factors', async () => {
    const gateway = await authenticator()
    await expect(gateway.authenticate({
      accessAssertion: assertion(),
      applicationCredential: 'credential-secret',
    })).resolves.toEqual({
      subject: 'owner-1',
      email: 'owner@example.test',
      edge_session_id: 'session-1',
      application_credential_id: 'credential-1',
    })
    await expect(gateway.authenticate({ accessAssertion: assertion(), applicationCredential: 'wrong' }))
      .rejects.toMatchObject({ code: 'unauthenticated' })
    await expect(gateway.authenticate({ accessAssertion: 'forged', applicationCredential: 'credential-secret' }))
      .rejects.toMatchObject({ code: 'unauthenticated' })
  })

  it('rejects revoked, expired, and subject-mismatched credentials', async () => {
    const digest = await digestApplicationCredential('credential-secret')
    const edge: EdgeIdentity = { subject: 'owner-1', email: 'owner@example.test', sessionId: 'session-1' }
    const validEdge = { verify: async () => edge }
    for (const record of [
      { id: 'revoked', subject: 'owner-1', secretDigest: digest, expiresAt: new Date(now.getTime() + 60_000), revokedAt: now },
      { id: 'expired', subject: 'owner-1', secretDigest: digest, expiresAt: new Date(now.getTime()), },
      { id: 'mismatch', subject: 'other-owner', secretDigest: digest, expiresAt: new Date(now.getTime() + 60_000) },
    ]) {
      const gateway = new DualFactorGatewayAuthenticator(
        validEdge,
        new RotatingApplicationCredentialVerifier([record], () => now),
        () => now,
      )
      await expect(gateway.authenticate({ accessAssertion: 'valid-edge', applicationCredential: 'credential-secret' }))
        .rejects.toBeInstanceOf(AuthenticationError)
    }
  })
})
