//! End-to-end SD-JWT VC: issue a credential, present a chosen subset under
//! key binding, verify it, and confirm the spoiling cases fail closed.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use aerocred_sdjwt::{
    IssuedSdJwt, IssuerParams, Presentation, PresentationParams, SigningKey, VerifyParams, issue,
    present, verify,
};
use chrono::{Duration, Utc};
use serde_json::{Map, Value, json};

const ISSUER: &str = "https://issuer.aerocred.example";
const SUBJECT: &str = "holder-7f3a91";
const VCT: &str = "https://aerocred.example/vc/airman-credential";
const AUDIENCE: &str = "https://proxy.aerocontext.example";
const NONCE: &str = "n-7f3a91c4";

struct Fixture {
    issued: IssuedSdJwt,
    issuer_key: SigningKey,
    holder_key: SigningKey,
    expires_at: chrono::DateTime<chrono::Utc>,
}

fn issue_fixture() -> Fixture {
    let issuer_key = SigningKey::generate();
    let holder_key = SigningKey::generate();
    let holder_jwk = holder_key.public_jwk();
    let now = Utc::now();
    let expires_at = now + Duration::hours(1);

    let mut selective = Map::new();
    selective.insert("holdsInstrumentRating".to_owned(), json!(true));
    selective.insert("pilotCertificateLevel".to_owned(), json!("commercial"));
    selective.insert("airmanCertificateNumber".to_owned(), json!("3148791"));

    let mut visible = Map::new();
    visible.insert("authorityClass".to_owned(), json!("bootstrap-operator"));

    let params = IssuerParams {
        issuer: ISSUER,
        subject: SUBJECT,
        vct: VCT,
        key_id: Some(issuer_key.public_jwk().thumbprint_sha256()),
        holder_key: &holder_jwk,
        issued_at: now,
        valid_from: now,
        valid_until: Some(expires_at),
        visible_claims: visible,
        selective_claims: selective,
        status: None,
    };
    let issued = issue(&params, &issuer_key).expect("issue credential");
    Fixture {
        issued,
        issuer_key,
        holder_key,
        expires_at,
    }
}

fn present_subset(fx: &Fixture, disclose: &[&str]) -> Presentation {
    let disclose: Vec<String> = disclose.iter().map(|s| (*s).to_owned()).collect();
    present(
        &fx.issued,
        &fx.holder_key,
        &PresentationParams {
            audience: AUDIENCE,
            nonce: NONCE,
            presented_at: Utc::now(),
            disclose: &disclose,
        },
    )
    .expect("build presentation")
}

#[test]
fn discloses_only_the_requested_claims() {
    let fx = issue_fixture();
    let presentation = present_subset(&fx, &["holdsInstrumentRating", "pilotCertificateLevel"]);

    let verified = verify(
        &presentation,
        &VerifyParams {
            issuer_key: &fx.issuer_key.verifying_key(),
            audience: AUDIENCE,
            nonce: NONCE,
            now: Utc::now(),
        },
    )
    .expect("verify presentation");

    assert_eq!(verified.issuer, ISSUER);
    assert_eq!(verified.subject, SUBJECT);
    assert_eq!(verified.vct, VCT);
    assert_eq!(
        verified.disclosed.get("holdsInstrumentRating"),
        Some(&Value::Bool(true))
    );
    assert_eq!(
        verified.disclosed.get("pilotCertificateLevel"),
        Some(&json!("commercial"))
    );
    // The withheld claim must not leak.
    assert!(!verified.disclosed.contains_key("airmanCertificateNumber"));
    // Visible issuer claims survive; standard claims are stripped out.
    assert_eq!(
        verified.visible.get("authorityClass"),
        Some(&json!("bootstrap-operator"))
    );
    assert!(!verified.visible.contains_key("iss"));
    // The verifier recovered the holder key it proved control of.
    assert_eq!(verified.holder_key, fx.holder_key.public_jwk());
}

#[test]
fn wrong_audience_is_rejected() {
    let fx = issue_fixture();
    let presentation = present_subset(&fx, &["holdsInstrumentRating"]);
    let result = verify(
        &presentation,
        &VerifyParams {
            issuer_key: &fx.issuer_key.verifying_key(),
            audience: "https://attacker.example",
            nonce: NONCE,
            now: Utc::now(),
        },
    );
    assert!(
        result.is_err(),
        "a replayed-to-wrong-verifier presentation must fail"
    );
}

#[test]
fn wrong_nonce_is_rejected() {
    let fx = issue_fixture();
    let presentation = present_subset(&fx, &["holdsInstrumentRating"]);
    let result = verify(
        &presentation,
        &VerifyParams {
            issuer_key: &fx.issuer_key.verifying_key(),
            audience: AUDIENCE,
            nonce: "stale-nonce",
            now: Utc::now(),
        },
    );
    assert!(
        result.is_err(),
        "a replayed presentation must fail the nonce check"
    );
}

#[test]
fn foreign_issuer_key_is_rejected() {
    let fx = issue_fixture();
    let presentation = present_subset(&fx, &["holdsInstrumentRating"]);
    let attacker = SigningKey::generate();
    let result = verify(
        &presentation,
        &VerifyParams {
            issuer_key: &attacker.verifying_key(),
            audience: AUDIENCE,
            nonce: NONCE,
            now: Utc::now(),
        },
    );
    assert!(
        result.is_err(),
        "a credential not signed by the trusted issuer must fail"
    );
}

#[test]
fn tampered_credential_is_rejected() {
    let fx = issue_fixture();
    let presentation = present_subset(&fx, &["holdsInstrumentRating"]);
    // Flip a character inside the issuer JWS payload segment.
    let raw = presentation.as_str();
    let mut bytes: Vec<char> = raw.chars().collect();
    let target = 10;
    bytes[target] = if bytes[target] == 'A' { 'B' } else { 'A' };
    let tampered = Presentation::from_string(bytes.into_iter().collect());
    let result = verify(
        &tampered,
        &VerifyParams {
            issuer_key: &fx.issuer_key.verifying_key(),
            audience: AUDIENCE,
            nonce: NONCE,
            now: Utc::now(),
        },
    );
    assert!(
        result.is_err(),
        "a tampered presentation must fail verification"
    );
}

#[test]
fn expired_credential_is_rejected() {
    let fx = issue_fixture();
    let presentation = present_subset(&fx, &["holdsInstrumentRating"]);
    let result = verify(
        &presentation,
        &VerifyParams {
            issuer_key: &fx.issuer_key.verifying_key(),
            audience: AUDIENCE,
            nonce: NONCE,
            now: fx.expires_at + Duration::seconds(1),
        },
    );
    assert!(result.is_err(), "verification past exp must fail");
}
