import { afterEach, describe, expect, it } from 'vitest'
import { mkdtempSync, mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createPublicKey, generateKeyPairSync, verify } from 'node:crypto'
import { generateIdentity, generateP256Identity } from '../src/keys/identity.js'
import { KeystoreError, openKeystore } from '../src/keys/keystore.js'

const dirs: string[] = []
function freshDir(): string {
  const d = mkdtempSync(join(tmpdir(), 'wta-ks-'))
  dirs.push(d)
  return d
}
afterEach(() => {
  while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
})

function mode(path: string): number {
  return statSync(path).mode & 0o777
}

describe('Keystore (INV4/INV5)', () => {
  it('persists the private key 0600 and reloads it', () => {
    const dir = freshDir()
    const ks = openKeystore(dir)
    const id = generateIdentity()
    ks.saveIdentity(id)
    expect(mode(join(dir, 'agent.key.pem'))).toBe(0o600)
    const reloaded = ks.loadIdentity()
    expect(reloaded).not.toBeNull()
    expect(Buffer.from(reloaded!.publicKey).equals(Buffer.from(id.publicKey))).toBe(true)
  })

  it('persists cert + CA chain 0600', () => {
    const dir = freshDir()
    const ks = openKeystore(dir)
    ks.saveCert('CERTPEM', 'CAPEM')
    expect(mode(join(dir, 'agent.cert.pem'))).toBe(0o600)
    expect(mode(join(dir, 'agent.ca.pem'))).toBe(0o600)
    expect(ks.loadCert()).toEqual({ certPem: 'CERTPEM', caChainPem: 'CAPEM' })
  })

  it('FIX 3: round-trips hostContentSecret 0600', () => {
    const dir = freshDir()
    const ks = openKeystore(dir)
    const secret = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
    ks.saveContentSecret(secret)
    expect(mode(join(dir, 'content.secret'))).toBe(0o600)
    expect(Buffer.from(ks.loadContentSecret()!).equals(Buffer.from(secret))).toBe(true)
  })

  it('returns null when nothing is stored yet', () => {
    const ks = openKeystore(freshDir())
    expect(ks.loadIdentity()).toBeNull()
    expect(ks.loadCert()).toBeNull()
    expect(ks.loadContentSecret()).toBeNull()
  })

  it('throws a typed error on a corrupt key file', () => {
    const dir = freshDir()
    writeFileSync(join(dir, 'agent.key.pem'), 'not-a-pem')
    expect(() => openKeystore(dir).loadIdentity()).toThrow(KeystoreError)
  })

  it('refuses to write into a group/world-accessible dir (INV5)', () => {
    const dir = join(freshDir(), 'wideopen')
    mkdirSync(dir, { mode: 0o755 })
    expect(() => openKeystore(dir).saveIdentity(generateIdentity())).toThrow(KeystoreError)
  })

  it('keeps the Ed25519 identity byte-identical across save/load (no P-256 regression)', () => {
    const dir = freshDir()
    const ks = openKeystore(dir)
    const id = generateIdentity()
    ks.saveIdentity(id)
    const reloaded = ks.loadIdentity()
    expect(reloaded!.alg).toBe('ed25519')
    expect(Buffer.from(reloaded!.publicKey).equals(Buffer.from(id.publicKey))).toBe(true)
    expect(reloaded!.enrollFpr).toBe(id.enrollFpr)
  })

  it('FIX H-host-2: round-trips a P-256 frp-client identity (alg + usable signing key)', () => {
    const dir = freshDir()
    const ks = openKeystore(dir)
    const id = generateP256Identity()
    ks.saveIdentity(id)
    expect(mode(join(dir, 'agent.key.pem'))).toBe(0o600)

    const reloaded = ks.loadIdentity()
    expect(reloaded).not.toBeNull()
    // loadIdentity() branched on the stored key's alg discriminant → P-256, not Ed25519.
    expect(reloaded!.alg).toBe('p256')
    // EC SubjectPublicKeyInfo DER (outer SEQUENCE) preserved exactly.
    expect(reloaded!.publicKey[0]).toBe(0x30)
    expect(Buffer.from(reloaded!.publicKey).equals(Buffer.from(id.publicKey))).toBe(true)
    expect(reloaded!.enrollFpr).toBe(id.enrollFpr)

    // The reloaded key still signs: a DER ECDSA signature that verifies under the original pubkey.
    const msg = new TextEncoder().encode('csr-bytes')
    const sig = reloaded!.sign(msg)
    const pub = createPublicKey({ key: Buffer.from(id.publicKey), format: 'der', type: 'spki' })
    expect(verify('sha256', msg, pub, sig)).toBe(true)
  })

  it('AG1: rejects a stored EC key on a non-P256 curve (e.g. secp384r1) with a clear error', () => {
    const dir = freshDir()
    const ks = openKeystore(dir)
    // Plant a valid PKCS#8 EC key on the WRONG curve directly at the key path — `ec` alone must not
    // be mistaken for P-256; loadIdentity must assert the named curve and fail closed.
    const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'secp384r1' })
    const pem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
    writeFileSync(join(dir, 'agent.key.pem'), pem, { mode: 0o600 })
    expect(() => ks.loadIdentity()).toThrow(KeystoreError)
    expect(() => ks.loadIdentity()).toThrow(/prime256v1|P-256|curve/)
  })
})
