/**
 * A5 native cert auto-renew wiring — TDD.
 *
 * Covers the host-side glue that was the "one real host code gap": the native run-loop must actually
 * RENEW the frp-client leaf (not merely monitor its freshness). Three units:
 *   - `createMtlsFetch` — the injected `fetchImpl` `renewCert` uses: it POSTs /renew over mTLS
 *     presenting the CURRENT keystore leaf (read fresh on every call, so a post-rotation renewal
 *     authenticates with the new leaf) and maps the transport response to a `Response`.
 *   - `wireAutoRenew` — routes the rotator's rotated→restartChild(+log), revoked→stop(+log),
 *     error→log(no secret) callbacks and starts it.
 *   - `startNativeAutoRenew` — the end-to-end builder `superviseNative` calls (identity + mTLS fetch
 *     + rotator), returning null (disabled) when unenrolled.
 */
import { describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { AgentConfig } from '../src/config/agentConfig.js'
import { generateP256Identity } from '../src/keys/identity.js'
import { openKeystore } from '../src/keys/keystore.js'
import { createLogger } from '../src/log/logger.js'
import type { CertRotator } from '../src/certs/rotation.js'
import {
  createMtlsFetch,
  startNativeAutoRenew,
  wireAutoRenew,
  type MtlsRequest,
} from '../src/certs/nativeRenew.js'
import { FakeTimer } from './fixtures/fakes.js'
import { CertExpiredBeyondGraceError } from '../src/certs/rotation.js'

const CFG: AgentConfig = {
  relayUrl: 'wss://relay/agent',
  enrollUrl: 'https://cp.example.com/enroll',
  stateDir: '/tmp/x',
  localTargetUrl: 'ws://127.0.0.1:3000',
  subdomain: 'host-42',
  hostId: 'h-1',
}

function enrolledKs(): { dir: string; ks: ReturnType<typeof openKeystore> } {
  const dir = mkdtempSync(join(tmpdir(), 'wta-nr-'))
  const ks = openKeystore(dir)
  ks.saveIdentity(generateP256Identity())
  ks.saveCert('LEAFCERT', 'CACHAIN')
  return { dir, ks }
}

const farFuture = (): { validTo: Date } => ({ validTo: new Date(Date.now() + 86_400_000) })
const flush = (): Promise<void> => new Promise((r) => setImmediate(r))

describe('createMtlsFetch (A5)', () => {
  it('presents the current keystore cert/key/CA over mTLS and maps the transport response', async () => {
    const { dir, ks } = enrolledKs()
    const seen: Array<{ url: string; tls: Record<string, unknown>; init: Record<string, unknown> }> = []
    const request: MtlsRequest = async (url, tls, init) => {
      seen.push({ url, tls: tls as unknown as Record<string, unknown>, init: init as unknown as Record<string, unknown> })
      return { status: 200, body: JSON.stringify({ cert: 'NEW', caChain: 'NEWCA' }) }
    }
    const f = createMtlsFetch(ks, { request, certParser: farFuture })

    const res = await f('https://cp.example.com/renew', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: '{"csr":"x"}',
    })

    expect(res.status).toBe(200)
    expect(res.ok).toBe(true)
    expect(await res.json()).toEqual({ cert: 'NEW', caChain: 'NEWCA' })
    expect(seen[0]!.url).toBe('https://cp.example.com/renew')
    expect(seen[0]!.tls.cert).toBe('LEAFCERT')
    // /renew verifies the LE-fronted control-plane against the SYSTEM roots, so no private CA is pinned
    // (pinning the enroll caChain here fails with "unable to get local issuer certificate").
    expect(seen[0]!.tls.ca).toBeUndefined()
    expect(seen[0]!.tls.rejectUnauthorized).toBe(true)
    expect(String(seen[0]!.tls.key)).toContain('PRIVATE KEY') // in-process PKCS#8 key, mTLS only
    expect(seen[0]!.init.method).toBe('POST')
    expect(seen[0]!.init.body).toBe('{"csr":"x"}')
    rmSync(dir, { recursive: true, force: true })
  })

  it('reads the current cert on every call so a post-rotation renewal uses the new leaf', async () => {
    const { dir, ks } = enrolledKs()
    const certs: string[] = []
    const request: MtlsRequest = async (_url, tls) => {
      certs.push(tls.cert)
      return { status: 200, body: '{}' }
    }
    const f = createMtlsFetch(ks, { request, certParser: farFuture })

    await f('https://x/renew', { method: 'POST' })
    ks.saveCert('ROTATEDLEAF', 'CACHAIN') // simulate a completed rotation
    await f('https://x/renew', { method: 'POST' })

    expect(certs).toEqual(['LEAFCERT', 'ROTATEDLEAF'])
    rmSync(dir, { recursive: true, force: true })
  })

  it('propagates a not-enrolled keystore as a throw (renewCert then retries with backoff)', async () => {
    const dir = mkdtempSync(join(tmpdir(), 'wta-nr-'))
    const ks = openKeystore(dir)
    const request: MtlsRequest = async () => ({ status: 200, body: '{}' })
    const f = createMtlsFetch(ks, { request, certParser: farFuture })
    await expect(f('https://x/renew', { method: 'POST' })).rejects.toThrow()
    rmSync(dir, { recursive: true, force: true })
  })
})

describe('wireAutoRenew (A5)', () => {
  function fakeRotator(): {
    rotator: CertRotator
    fire: {
      rotated?: () => void
      revoked?: () => void
      error?: (e: unknown) => void
      exhausted?: (e: CertExpiredBeyondGraceError) => void
    }
    start: ReturnType<typeof vi.fn>
    stop: ReturnType<typeof vi.fn>
  } {
    const fire: {
      rotated?: () => void
      revoked?: () => void
      error?: (e: unknown) => void
      exhausted?: (e: CertExpiredBeyondGraceError) => void
    } = {}
    const start = vi.fn()
    const stop = vi.fn()
    const rotator: CertRotator = {
      start,
      stop,
      onRotated: (cb) => {
        fire.rotated = cb
      },
      onRevoked: (cb) => {
        fire.revoked = cb
      },
      onError: (cb) => {
        fire.error = cb
      },
      onExhausted: (cb) => {
        fire.exhausted = cb
      },
    }
    return { rotator, fire, start, stop }
  }

  it('routes rotated→restartChild, revoked→stop, error→log; starts and stops the rotator', () => {
    const { rotator, fire, start, stop } = fakeRotator()
    const restartChild = vi.fn()
    const stopSupervisor = vi.fn()
    const lines: string[] = []
    const logger = createLogger('info', (l) => lines.push(l))

    const controller = wireAutoRenew(rotator, { restartChild, stop: stopSupervisor }, logger, {
      subdomain: 'host-42',
      hostId: 'h-1',
    })
    expect(start).toHaveBeenCalledTimes(1)

    fire.rotated!()
    expect(restartChild).toHaveBeenCalledTimes(1)

    fire.revoked!()
    expect(stopSupervisor).toHaveBeenCalledTimes(1)

    fire.error!(new Error('network down'))

    controller.stop()
    expect(stop).toHaveBeenCalledTimes(1)

    const joined = lines.join('\n')
    expect(joined).toContain('host-42') // non-secret identifier is logged
    expect(joined).not.toContain('LEAFCERT') // never a leaf/key/CSR
  })
})

describe('startNativeAutoRenew (A5 end-to-end)', () => {
  it('a scheduled 200 renewal rotates the leaf on disk and restarts frpc with it', async () => {
    const { dir, ks } = enrolledKs()
    const timer = new FakeTimer()
    const request: MtlsRequest = async () => ({
      status: 200,
      body: JSON.stringify({ cert: 'FRESHLEAF', caChain: ['CACHAIN'] }),
    })
    const restartChild = vi.fn()
    const stop = vi.fn()

    const controller = startNativeAutoRenew(
      CFG,
      ks,
      { restartChild, stop },
      createLogger('error', () => {}),
      { mtlsRequest: request, certParser: farFuture, timer, renewBeforeMs: 1000, now: () => new Date(0), parseCert: () => new Date(2000) },
    )!
    expect(controller).not.toBeNull()

    timer.advance(1000) // renewal fires at ~2/3 TTL
    await flush()

    expect(ks.loadCert()!.certPem).toContain('FRESHLEAF') // atomic persist
    expect(restartChild).toHaveBeenCalledTimes(1) // frpc restarted onto the fresh leaf
    expect(stop).not.toHaveBeenCalled()
    controller.stop()
    rmSync(dir, { recursive: true, force: true })
  })

  it('a scheduled 403 renewal tears the tunnel down (revoked) and never rotates', async () => {
    const { dir, ks } = enrolledKs()
    const timer = new FakeTimer()
    const request: MtlsRequest = async () => ({ status: 403, body: '' })
    const restartChild = vi.fn()
    const stop = vi.fn()

    const controller = startNativeAutoRenew(
      CFG,
      ks,
      { restartChild, stop },
      createLogger('error', () => {}),
      { mtlsRequest: request, certParser: farFuture, timer, renewBeforeMs: 1000, now: () => new Date(0), parseCert: () => new Date(2000) },
    )!

    timer.advance(1000)
    await flush()

    expect(stop).toHaveBeenCalledTimes(1)
    expect(restartChild).not.toHaveBeenCalled()
    expect(ks.loadCert()!.certPem).toBe('LEAFCERT') // untouched
    controller.stop()
    rmSync(dir, { recursive: true, force: true })
  })

  it('a failing renewal is logged (no secret) and retried without crashing, then rotates', async () => {
    const { dir, ks } = enrolledKs()
    const timer = new FakeTimer()
    let calls = 0
    const request: MtlsRequest = async () => {
      calls += 1
      if (calls === 1) throw new Error('ECONNREFUSED')
      return { status: 200, body: JSON.stringify({ cert: 'FRESHLEAF', caChain: ['CACHAIN'] }) }
    }
    const restartChild = vi.fn()
    const stop = vi.fn()
    const lines: string[] = []

    const controller = startNativeAutoRenew(
      CFG,
      ks,
      { restartChild, stop },
      createLogger('warn', (l) => lines.push(l)),
      {
        mtlsRequest: request,
        certParser: farFuture,
        timer,
        renewBeforeMs: 1000,
        retryBaseMs: 500,
        now: () => new Date(0),
        parseCert: () => new Date(2000),
      },
    )!

    timer.advance(1000) // first attempt throws
    await flush()
    expect(restartChild).not.toHaveBeenCalled()
    expect(stop).not.toHaveBeenCalled() // a failure NEVER tears down
    expect(lines.join('\n')).toContain('retry')

    timer.advance(500) // backoff retry fires and succeeds
    await flush()
    expect(ks.loadCert()!.certPem).toContain('FRESHLEAF')
    expect(restartChild).toHaveBeenCalledTimes(1)
    expect(lines.join('\n')).not.toContain('LEAFCERT')
    controller.stop()
    rmSync(dir, { recursive: true, force: true })
  })

  it('returns null (auto-renew disabled) when the keystore has no identity', () => {
    const dir = mkdtempSync(join(tmpdir(), 'wta-nr-'))
    const ks = openKeystore(dir)
    const lines: string[] = []
    const controller = startNativeAutoRenew(
      CFG,
      ks,
      { restartChild: () => {}, stop: () => {} },
      createLogger('warn', (l) => lines.push(l)),
      {},
    )
    expect(controller).toBeNull()
    expect(lines.join('\n')).toContain('auto-renew disabled')
    rmSync(dir, { recursive: true, force: true })
  })
})

/**
 * The mTLS renew transport stays STRICT about expiry: nginx refuses to forward an expired client
 * cert at all, so an expired leaf must be routed to the plain `/recover` endpoint by the rotator
 * rather than smuggled through this transport.
 */
describe('createMtlsFetch stays fail-closed on an expired leaf', () => {
  it('refuses to present a lapsed leaf (recovery is the rotator\'s job, not this transport\'s)', async () => {
    const { dir, ks } = enrolledKs()
    let called = 0
    const request: MtlsRequest = async () => {
      called += 1
      return { status: 201, body: '{}' }
    }
    const f = createMtlsFetch(ks, {
      request,
      certParser: () => ({ validTo: new Date(Date.now() - 86_400_000) }),
    })
    await expect(f('https://cp.example.com/renew', { method: 'POST' })).rejects.toThrow(
      /expired/i,
    )
    expect(called).toBe(0)
    rmSync(dir, { recursive: true, force: true })
  })
})

describe('wireAutoRenew exhausted routing', () => {
  it('logs an actionable re-pair alarm and leaves the supervisor running', () => {
    const fire: { exhausted?: (e: CertExpiredBeyondGraceError) => void } = {}
    const rotator: CertRotator = {
      start: vi.fn(),
      stop: vi.fn(),
      onRotated: () => {},
      onRevoked: () => {},
      onError: () => {},
      onExhausted: (cb) => {
        fire.exhausted = cb
      },
    }
    const restartChild = vi.fn()
    const stop = vi.fn()
    const lines: string[] = []
    wireAutoRenew(rotator, { restartChild, stop }, createLogger('info', (l) => lines.push(l)), {
      subdomain: 'h7fd8',
      hostId: 'h-1',
    })
    fire.exhausted?.(new CertExpiredBeyondGraceError(40 * 86_400_000, 30 * 86_400_000))

    const alarm = lines.find((l) => /re-pair/i.test(l))
    expect(alarm).toBeDefined()
    expect(alarm).toContain('h7fd8')
    // The supervisor keeps running: a later `pair` writes fresh cert files that the restarting
    // frpc child picks up. Tearing down here would make recovery need a manual restart too.
    expect(stop).not.toHaveBeenCalled()
    expect(restartChild).not.toHaveBeenCalled()
  })
})
