//! Integration tests for trust anchor NameConstraints seeding.
//!
//! Oracle: DER fixtures generated by gen.py (pyca/cryptography) in
//! `tests/fixtures/anchor-nc/`. See the README in that directory for details
//! on what each fixture encodes and what the expected outcomes are.
//!
//! # Tests
//!
//! 1. `valid_leaf_passes_permitted_nc` — leaf whose SAN is inside the anchor's
//!    permittedSubtrees accepts validation.
//! 2. `leaf_violating_permitted_fails` — leaf whose SAN is outside the anchor's
//!    permittedSubtrees is rejected with `NameConstraintViolation`.
//! 3. `leaf_in_excluded_subtree_fails` — leaf whose SAN matches the anchor's
//!    excludedSubtrees is rejected with `NameConstraintViolation`.
//! 4. `try_from_valid_nc_cert_populates_name_constraints` — `TryFrom<Certificate>`
//!    on a cert that has a well-formed NC extension returns `Ok` with
//!    `name_constraints = Some(...)`.
//! 5. `from_cert_valid_nc_populates_name_constraints` — `TrustAnchor::from_cert`
//!    on a cert with a well-formed NC extension sets `name_constraints = Some(...)`.

use der::{asn1::OctetString, Decode as _};
use pkix_path::{DefaultVerifier, TrustAnchor, ValidationPolicy};
use x509_cert::Certificate;

/// Unix timestamp within the fixture validity window (2000-01-01 to 2050-01-01).
///
/// Using 2025-01-01 00:00:00 UTC = 1_735_689_600.
const ANCHOR_NC_NOW: u64 = 1_735_689_600;

/// OID for the NameConstraints extension (RFC 5280 §4.2.1.10).
const OID_NAME_CONSTRAINTS: der::asn1::ObjectIdentifier =
    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.30");

fn load_cert(path: &str) -> Certificate {
    let der = std::fs::read(path).unwrap_or_else(|e| panic!("fixture not found: {path}: {e}"));
    Certificate::from_der(&der).unwrap_or_else(|e| panic!("invalid cert DER in {path}: {e}"))
}

fn anchor_nc_fixture(name: &str) -> String {
    format!(
        "{}/tests/fixtures/anchor-nc/{name}",
        env!("CARGO_MANIFEST_DIR")
    )
}

fn policy() -> ValidationPolicy {
    ValidationPolicy::new(ANCHOR_NC_NOW)
}

/// Test 1: valid leaf passes anchor permitted NC.
///
/// Oracle: anchor_permitted_dns has permittedSubtrees=.example.com;
/// leaf_good_dns has SAN=www.example.com — inside the permitted subtree.
/// Expected: validate_path returns Ok.
#[test]
fn valid_leaf_passes_permitted_nc() {
    let anchor_cert = load_cert(&anchor_nc_fixture("anchor_permitted_dns.der"));
    let leaf = load_cert(&anchor_nc_fixture("leaf_good_dns.der"));

    let anchor = TrustAnchor::from_cert(anchor_cert);
    assert!(
        anchor.name_constraints.is_some(),
        "anchor must have name_constraints populated from cert"
    );

    let result = pkix_path::validate_path(&[leaf], &[anchor], &policy(), &DefaultVerifier);
    result.expect("www.example.com is inside .example.com permitted subtree; must validate");
}

/// Test 2: leaf violating permitted subtrees fails.
///
/// Oracle: anchor_permitted_dns has permittedSubtrees=.example.com;
/// leaf_bad_dns has SAN=www.evil.com — outside the permitted subtree.
/// Expected: validate_path returns Err(NameConstraintViolation).
#[test]
fn leaf_violating_permitted_fails() {
    let anchor_cert = load_cert(&anchor_nc_fixture("anchor_permitted_dns.der"));
    let leaf = load_cert(&anchor_nc_fixture("leaf_bad_dns.der"));

    let anchor = TrustAnchor::from_cert(anchor_cert);

    let result = pkix_path::validate_path(&[leaf], &[anchor], &policy(), &DefaultVerifier);
    assert!(
        matches!(
            result,
            Err(pkix_path::Error::NameConstraintViolation { .. })
        ),
        "www.evil.com not in .example.com permitted subtree must return NameConstraintViolation; \
         got: {result:?}"
    );
}

/// Test 3: leaf in excluded subtrees fails.
///
/// Oracle: anchor_excluded_dns has excludedSubtrees=evil.example.com;
/// leaf_excluded_dns has SAN=evil.example.com — inside the excluded subtree.
/// Expected: validate_path returns Err(NameConstraintViolation).
#[test]
fn leaf_in_excluded_subtree_fails() {
    let anchor_cert = load_cert(&anchor_nc_fixture("anchor_excluded_dns.der"));
    let leaf = load_cert(&anchor_nc_fixture("leaf_excluded_dns.der"));

    let anchor = TrustAnchor::from_cert(anchor_cert);
    assert!(
        anchor.name_constraints.is_some(),
        "anchor must have name_constraints populated from cert"
    );

    let result = pkix_path::validate_path(&[leaf], &[anchor], &policy(), &DefaultVerifier);
    assert!(
        matches!(
            result,
            Err(pkix_path::Error::NameConstraintViolation { .. })
        ),
        "evil.example.com in excluded subtree must return NameConstraintViolation; \
         got: {result:?}"
    );
}

/// Test 4: TryFrom<Certificate> on a cert with a malformed NC extension returns Err.
///
/// Construct a Certificate that has the NameConstraints OID but with a garbage
/// extn_value (0xFF 0xFF is not valid DER for NameConstraints). TryFrom must
/// return Err(der::Error) — fail-closed behaviour.
#[test]
fn try_from_malformed_nc_returns_err() {
    let mut cert = load_cert(&anchor_nc_fixture("anchor_permitted_dns.der"));

    // Replace the NC extension's value with invalid DER bytes.
    let exts = cert
        .tbs_certificate
        .extensions
        .as_mut()
        .expect("fixture must have extensions");
    for ext in exts.iter_mut() {
        if ext.extn_id == OID_NAME_CONSTRAINTS {
            ext.extn_value = OctetString::new(b"\xff\xff").expect("OctetString::new");
        }
    }

    let result = TrustAnchor::try_from(cert);
    assert!(
        result.is_err(),
        "TryFrom with malformed NC extension must return Err; got Ok"
    );
}

/// Test 5: TrustAnchor::from_cert on a cert with a malformed NC extension gives
/// name_constraints = None (fail-open / infallible behaviour).
///
/// Uses the same garbage-extension technique as test 4. The infallible
/// from_cert silently treats parse errors as absent.
#[test]
fn from_cert_malformed_nc_gives_none() {
    let mut cert = load_cert(&anchor_nc_fixture("anchor_permitted_dns.der"));

    // Replace the NC extension's value with invalid DER bytes.
    let exts = cert
        .tbs_certificate
        .extensions
        .as_mut()
        .expect("fixture must have extensions");
    for ext in exts.iter_mut() {
        if ext.extn_id == OID_NAME_CONSTRAINTS {
            ext.extn_value = OctetString::new(b"\xff\xff").expect("OctetString::new");
        }
    }

    let anchor = TrustAnchor::from_cert(cert);
    assert_eq!(
        anchor.name_constraints, None,
        "from_cert with malformed NC must silently produce name_constraints = None"
    );
}
