/**
 * B1 — operator login → device:enroll bearer mint. Proves the PINNED contract:
 *   POST /auth/login { password } -> 201 { enrollToken, accountId, expiresIn }
 *   then POST /device/enroll  Authorization: Bearer <enrollToken>  is reachable (201) and rejected
 *   WITHOUT the bearer (401).
 *
 * Security surface under test: constant-time credential compare (via loginToAccountId), rate-limited
 * login attempts, deny-by-default / fail-closed when the operator credential is unset, and that the
 * minted bearer carries the correct right ('enroll') + audience ('device-enroll') + TTL — verified
 * through relay-auth's REAL §4.3 verify path (verifyDeviceEnrollToken), the same one production uses.
 */
import 'reflect-metadata'
import { describe, test, expect, beforeEach } from 'vitest'
import * as x509 from '@peculiar/x509'
import { webcrypto } from 'node:crypto'
import Fastify, { type FastifyInstance } from 'fastify'
import { configureVerifyKey } from 'relay-auth'
import { resetVerifyKeyForTest } from 'relay-auth/src/config/keys.js'
import { generateEd25519KeyPair, exportEd25519PublicRaw } from 'relay-auth/src/crypto/ed25519.js'
import { buildAuthLoginRouter, type AuthLoginDeps } from '../src/api/auth-login.js'
import {
  verifyDeviceEnrollToken,
  DEVICE_ENROLL_AUD,
  DEFAULT_DEVICE_ENROLL_TTL_SEC,
} from '../src/auth/session.js'
import { buildControlPlane } from '../src/main.js'
import { createCapabilityVerifier } from '../src/boot/verifier.js'
import { loadEnv } from '../src/env.js'
import { createMemoryStores } from '../src/store/memory.js'
import { createHostRegistry } from '../src/registry/hosts.js'
import { fingerprint } from '../src/ca/fingerprint.js'
import { generateEd25519 } from '../src/util/crypto.js'
import { buildCsrEc } from '../src/ca/csr-ec.js'
import { bytesToBase64 } from '../src/util/bytes.js'

x509.cryptoProvider.set(webcrypto)

const ACCOUNT_A = '11111111-1111-4111-8111-111111111111'
const OPERATOR_PASSWORD = 'operator-secret-abcdef123456'

// ───────────────────────────────────────────────────────────────────────────────────────────────
// Block A — the login route in isolation (fast; verify-key configured so the mint round-trips)
// ───────────────────────────────────────────────────────────────────────────────────────────────
describe('B1 POST /auth/login (route in isolation)', () => {
  let signingKey: CryptoKey

  beforeEach(async () => {
    resetVerifyKeyForTest()
    const pair = await generateEd25519KeyPair()
    await configureVerifyKey(pair.publicKey)
    signingKey = pair.privateKey
  })

  function buildApp(overrides: Partial<AuthLoginDeps> = {}): FastifyInstance {
    const deps: AuthLoginDeps = {
      signingKey,
      loginConfig: { operatorCredential: OPERATOR_PASSWORD, accountId: ACCOUNT_A },
      clientKey: () => 'test-client', // deterministic rate-limit bucket
      ...overrides,
    }
    const app = Fastify({ logger: false })
    void app.register(buildAuthLoginRouter(deps))
    return app
  }

  test('valid password → 201 { enrollToken, accountId, expiresIn }', async () => {
    const app = buildApp()
    await app.ready()
    const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { password: OPERATOR_PASSWORD } })
    expect(res.statusCode).toBe(201)
    const body = JSON.parse(res.body)
    expect(body.accountId).toBe(ACCOUNT_A)
    expect(body.expiresIn).toBe(DEFAULT_DEVICE_ENROLL_TTL_SEC)
    expect(typeof body.enrollToken).toBe('string')
    expect(body.enrollToken.startsWith('v4.public.')).toBe(true)
  })

  test('the minted bearer carries the enroll right + device-enroll aud + minutes TTL', async () => {
    const app = buildApp()
    await app.ready()
    const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { password: OPERATOR_PASSWORD } })
    const { enrollToken, expiresIn } = JSON.parse(res.body)
    // A successful verify through the REAL §4.3 path asserts aud === device-enroll AND the enroll right.
    const { accountId } = await verifyDeviceEnrollToken(enrollToken, { aud: DEVICE_ENROLL_AUD })
    expect(accountId).toBe(ACCOUNT_A)
    expect(expiresIn).toBeGreaterThanOrEqual(60) // minutes-scale, separate from the 30–60s connect clamp
  })

  test('wrong password → 401, no token minted', async () => {
    const app = buildApp()
    await app.ready()
    const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { password: 'wrong-but-long-enough' } })
    expect(res.statusCode).toBe(401)
    expect(JSON.parse(res.body).enrollToken).toBeUndefined()
  })

  test('missing password field → 400 (Zod at the boundary)', async () => {
    const app = buildApp()
    await app.ready()
    const res = await app.inject({ method: 'POST', url: '/auth/login', payload: {} })
    expect(res.statusCode).toBe(400)
  })

  test('fail-closed when the operator credential is unset → 503, never mints', async () => {
    const app = buildApp({ signingKey: null, loginConfig: {} })
    await app.ready()
    const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { password: OPERATOR_PASSWORD } })
    expect(res.statusCode).toBe(503)
    expect(JSON.parse(res.body).enrollToken).toBeUndefined()
  })

  test('login attempts are rate-limited → 429 after the threshold', async () => {
    const app = buildApp({ rateMax: 2 })
    await app.ready()
    const attempt = () =>
      app.inject({ method: 'POST', url: '/auth/login', payload: { password: 'wrong-but-long-enough' } })
    expect((await attempt()).statusCode).toBe(401)
    expect((await attempt()).statusCode).toBe(401)
    expect((await attempt()).statusCode).toBe(429) // over the per-client window
  })
})

// ───────────────────────────────────────────────────────────────────────────────────────────────
// Block B — end-to-end through the WIRED control-plane (login → bearer → /device/enroll)
// ───────────────────────────────────────────────────────────────────────────────────────────────
describe('B1 login → device:enroll e2e (wired app)', () => {
  let app: FastifyInstance

  beforeEach(async () => {
    resetVerifyKeyForTest()
    // Capability keypair: raw public → boot verify key; PKCS#8 private → the enroll signing key env.
    const pair = await generateEd25519KeyPair()
    const rawPub = await exportEd25519PublicRaw(pair.publicKey)
    const pkcs8 = new Uint8Array(await webcrypto.subtle.exportKey('pkcs8', pair.privateKey))
    const env = loadEnv({
      PG_URL: 'postgres://u:p@localhost:5432/cp',
      REDIS_URL: 'redis://localhost:6379',
      CAPABILITY_SIGN_PUBKEY_B64: bytesToBase64(rawPub),
      CAPABILITY_SIGN_PRIVKEY_B64: bytesToBase64(pkcs8),
      OPERATOR_PASSWORD,
      OPERATOR_ACCOUNT_ID: ACCOUNT_A,
      CA_INTERMEDIATE_KMS_KEY_REF: 'kms://key/intermediate',
      CA_INTERMEDIATE_CERT_PATH: '/etc/cp/int.pem',
      NODE_MTLS_TRUST_BUNDLE_PATH: '/etc/cp/node-ca.pem',
      RELAY_TRUST_DOMAIN: 'terminal.yaojia.wang',
      BASE_DOMAIN: 'term.example.com',
    })
    const stores = createMemoryStores()
    const built = await buildControlPlane(env, { stores, verifier: createCapabilityVerifier() })
    app = built.app
    await app.ready()
    // Onboard a host so ACCOUNT_A OWNS 'alice' (device-enroll ownership gate reads this registry).
    const hosts = createHostRegistry({ hosts: stores.hosts })
    const { publicKeyRaw } = generateEd25519()
    await hosts.bindHost({ accountId: ACCOUNT_A, subdomain: 'alice', agentPubkey: publicKeyRaw, enrollFpr: fingerprint(publicKeyRaw) })
  })

  test('login mints a bearer that /device/enroll accepts (201); without it → 401', async () => {
    const login = await app.inject({ method: 'POST', url: '/auth/login', payload: { password: OPERATOR_PASSWORD } })
    expect(login.statusCode).toBe(201)
    const { enrollToken, accountId } = JSON.parse(login.body)
    expect(accountId).toBe(ACCOUNT_A)

    const { der: csr } = await buildCsrEc('CN=web-terminal-device')
    const payload = { csr: Buffer.from(csr).toString('base64'), keyAlg: 'ec-p256', subdomain: 'alice', deviceName: 'iphone' }

    const enrolled = await app.inject({
      method: 'POST',
      url: '/device/enroll',
      headers: { authorization: `Bearer ${enrollToken}` },
      payload,
    })
    expect(enrolled.statusCode).toBe(201)
    expect(JSON.parse(enrolled.body).deviceId.length).toBeGreaterThan(0)

    const noBearer = await app.inject({ method: 'POST', url: '/device/enroll', payload })
    expect(noBearer.statusCode).toBe(401)
  })

  test('wrong operator password on the wired app → 401', async () => {
    const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { password: 'definitely-the-wrong-secret' } })
    expect(res.statusCode).toBe(401)
  })
})
