//! Integration tests for `pkix_revocation::CrlChecker`.
//!
//! All DER fixtures in tests/fixtures/ were generated by gen_crl_fixtures.py
//! using pyca/cryptography as the external oracle. Tests run fully offline.
//!
//! Fixture structure:
//!   crl-ca.der             — RSA-2048 self-signed CA (serial=100)
//!   crl-leaf-good.der      — leaf cert, serial=1 (not revoked)
//!   crl-leaf-revoked.der   — leaf cert, serial=2 (appears in revocation CRLs)
//!   crl-empty.der          — empty CRL, valid 2026-01-01..2027-01-01
//!   crl-with-revocation.der — CRL with serial=2 revoked, no reason code
//!   crl-with-reason.der    — CRL with serial=2 revoked, CRLReason=keyCompromise(1)
//!   crl-expired.der        — CRL whose nextUpdate=2021-01-01 (expired)
//!   crl-bad-sig.der        — crl-empty.der with last signature byte corrupted
//!
//! Validation time: 2026-06-01 00:00:00 UTC = 1_780_272_000.
//! All valid CRLs have thisUpdate=2026-01-01, nextUpdate=2027-01-01.

use der::Decode as _;
use pkix_path::DefaultVerifier;
use pkix_revocation::{CrlChecker, Error, RevocationChecker as _};
use x509_cert::{ext::pkix::crl::CrlReason, Certificate};

/// Unix timestamp used as "now" for all tests (2026-06-01 00:00:00 UTC).
const NOW: u64 = 1_780_272_000;

fn fixture(name: &str) -> Vec<u8> {
    let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(name);
    std::fs::read(&path).unwrap_or_else(|e| panic!("read {name}: {e}"))
}

fn load_cert(name: &str) -> Certificate {
    let der = fixture(name);
    Certificate::from_der(&der).unwrap_or_else(|e| panic!("parse {name}: {e}"))
}

fn checker(crl_name: &str) -> CrlChecker<DefaultVerifier> {
    CrlChecker::new(fixture(crl_name), NOW, DefaultVerifier)
}

// ---------------------------------------------------------------------------
// Happy path — no revocation
// ---------------------------------------------------------------------------

/// Good cert (serial=1) against an empty CRL → Ok(()).
///
/// Oracle: pyca/cryptography; crl-empty.der has no revoked entries.
#[test]
fn crl_good_cert_empty_crl_ok() {
    let ca = load_cert("crl-ca.der");
    let good = load_cert("crl-leaf-good.der");
    let result = checker("crl-empty.der").check_revocation(&good, &ca);
    result.expect("good cert against empty CRL must return Ok(())");
}

/// Good cert (serial=1) against a CRL that revokes serial=2 → Ok(()).
///
/// Oracle: serial 1 does not appear in crl-with-revocation.der.
#[test]
fn crl_good_cert_not_in_revoked_list_ok() {
    let ca = load_cert("crl-ca.der");
    let good = load_cert("crl-leaf-good.der");
    let result = checker("crl-with-revocation.der").check_revocation(&good, &ca);
    result.expect("serial=1 cert against CRL revoking serial=2 must return Ok(())");
}

// ---------------------------------------------------------------------------
// Revoked
// ---------------------------------------------------------------------------

/// Revoked cert (serial=2) against crl-with-revocation.der → Err(Revoked) with no reason.
///
/// Oracle: pyca/cryptography; revoked entry has no CRLReason extension.
#[test]
fn crl_cert_revoked_no_reason() {
    let ca = load_cert("crl-ca.der");
    let revoked = load_cert("crl-leaf-revoked.der");
    let result = checker("crl-with-revocation.der").check_revocation(&revoked, &ca);
    assert!(
        matches!(
            result,
            Err(Error::Revoked {
                reason_code: None,
                ..
            })
        ),
        "revoked cert (no reason) must return Revoked {{ reason_code: None }}, got: {result:?}"
    );
}

/// Revoked cert (serial=2) against crl-with-reason.der → Err(Revoked) with reason=KeyCompromise.
///
/// Oracle: pyca/cryptography; CRLReason=keyCompromise (RFC 5280 §5.3.1 value 1).
#[test]
fn crl_cert_revoked_with_reason_key_compromise() {
    let ca = load_cert("crl-ca.der");
    let revoked = load_cert("crl-leaf-revoked.der");
    let result = checker("crl-with-reason.der").check_revocation(&revoked, &ca);
    assert!(
        matches!(result, Err(Error::Revoked { reason_code: Some(CrlReason::KeyCompromise), .. })),
        "revoked cert (keyCompromise) must return Revoked {{ reason_code: Some(KeyCompromise) }}, got: {result:?}"
    );
}

// ---------------------------------------------------------------------------
// CRL validity window
// ---------------------------------------------------------------------------

/// Valid cert against an expired CRL (nextUpdate=2021-01-01 < NOW=2026-06-01) → Err(CrlExpired).
///
/// Oracle: crl-expired.der has nextUpdate=2021-01-01 08:00:00 UTC.
#[test]
fn crl_expired_returns_error() {
    let ca = load_cert("crl-ca.der");
    let good = load_cert("crl-leaf-good.der");
    let result = checker("crl-expired.der").check_revocation(&good, &ca);
    assert!(
        matches!(result, Err(Error::CrlExpired)),
        "expired CRL must return CrlExpired, got: {result:?}"
    );
}

// ---------------------------------------------------------------------------
// Signature errors
// ---------------------------------------------------------------------------

/// Valid cert against a CRL with a corrupted signature → Err(CrlSignatureInvalid).
///
/// Oracle: crl-bad-sig.der is crl-empty.der with last signature byte XOR'd 0xFF.
#[test]
fn crl_bad_signature_returns_error() {
    let ca = load_cert("crl-ca.der");
    let good = load_cert("crl-leaf-good.der");
    let result = checker("crl-bad-sig.der").check_revocation(&good, &ca);
    assert!(
        matches!(result, Err(Error::CrlSignatureInvalid)),
        "corrupted CRL signature must return CrlSignatureInvalid, got: {result:?}"
    );
}

// ---------------------------------------------------------------------------
// Parse errors
// ---------------------------------------------------------------------------

/// Truncated CRL DER → Err(CrlParseError(_)).
#[test]
fn crl_parse_error_truncated_der() {
    let ca = load_cert("crl-ca.der");
    let good = load_cert("crl-leaf-good.der");
    let truncated = vec![0x30, 0x82, 0x01, 0x00, 0xAA, 0xBB]; // garbage
    let checker = CrlChecker::new(truncated, NOW, DefaultVerifier);
    let result = checker.check_revocation(&good, &ca);
    assert!(
        matches!(result, Err(Error::CrlParseError(_))),
        "truncated CRL must return CrlParseError, got: {result:?}"
    );
}
