//! Exercises every stable invariant in the failure taxonomy at least once.

mod common;

use chrono::{Duration, Utc};
use cortex_core::{
    AuthorityClass, ClaimCeiling, ClaimProofState, PolicyContribution, PolicyDecision,
    PolicyOutcome, RuntimeMode,
};
use cortex_verifier::{
    verify, verify_with_policy, EvidenceInput, VerifiedTrustState, WitnessClass, WitnessSignature,
    WitnessTier, COMPOSITION_CEILING_BELOW_REQUIRED, COMPOSITION_POLICY_FAIL_CLOSED,
    WITNESS_DISAGREEMENT, WITNESS_MISSING, WITNESS_SIGNATURE_INVALID, WITNESS_STALE,
    WITNESS_TIER_INSUFFICIENT,
};

use crate::common::{
    advisory_only_input, authority_grade_input, full_witness_set, sample_evidence_digest,
    sample_payload, signed_witness, Kind,
};

#[test]
fn missing_required_classes_produce_partial_with_witness_missing_reason() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let witnesses = vec![signed_witness(
        1,
        WitnessClass::SignedLedgerChainHead,
        WitnessTier::OperatorOwned,
        &digest,
        now,
    )];

    let state = verify(
        &authority_grade_input(Kind::ReleaseReadiness),
        &witnesses,
        now,
        Duration::seconds(86_400),
    );

    match state {
        VerifiedTrustState::Partial { reasons, .. } => {
            assert!(reasons.iter().any(|r| r.contains(WITNESS_MISSING)));
            assert!(reasons
                .iter()
                .any(|r| r.contains("external_anchor_crossing")));
            assert!(reasons.iter().any(|r| r.contains("remote_ci_conclusion")));
            assert!(reasons
                .iter()
                .any(|r| r.contains("reproducible_build_provenance")));
        }
        other => panic!("expected Partial(witness_missing), got {other:?}"),
    }
}

#[test]
fn subject_mismatch_is_witness_disagreement() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let mut witnesses = full_witness_set(&digest, now);
    witnesses[2].asserted_subject_blake3 = "0".repeat(64);

    let state = verify(
        &authority_grade_input(Kind::ReleaseReadiness),
        &witnesses,
        now,
        Duration::seconds(86_400),
    );

    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, WITNESS_DISAGREEMENT);
        }
        other => panic!("expected Broken(witness_disagreement), got {other:?}"),
    }
}

#[test]
fn stale_witness_is_rejected() {
    let digest = sample_evidence_digest();
    let asserted_at = Utc::now() - Duration::hours(48);
    let witnesses = full_witness_set(&digest, asserted_at);

    let state = verify(
        &authority_grade_input(Kind::ReleaseReadiness),
        &witnesses,
        Utc::now(),
        Duration::seconds(86_400),
    );

    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, WITNESS_STALE);
            assert!(edge.detail.contains("stale"));
        }
        other => panic!("expected Broken(witness_stale), got {other:?}"),
    }
}

#[test]
fn local_tier_remote_ci_is_tier_insufficient() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let mut witnesses = full_witness_set(&digest, now);
    // Drop the remote-CI witness's tier below ThirdParty.
    let downgraded = signed_witness(
        3,
        WitnessClass::RemoteCiConclusion,
        WitnessTier::Local,
        &digest,
        now,
    );
    witnesses[2] = downgraded;

    let state = verify(
        &authority_grade_input(Kind::ReleaseReadiness),
        &witnesses,
        now,
        Duration::seconds(86_400),
    );

    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, WITNESS_TIER_INSUFFICIENT);
            assert!(edge.detail.contains("remote_ci_conclusion"));
        }
        other => panic!("expected Broken(tier_insufficient), got {other:?}"),
    }
}

#[test]
fn signature_invalid_when_signature_bytes_corrupted() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let mut witnesses = full_witness_set(&digest, now);
    // Corrupt one signature byte in the Ed25519 variant.
    match &mut witnesses[0].signature {
        WitnessSignature::Ed25519 {
            signature_bytes, ..
        } => {
            signature_bytes[0] ^= 0x01;
        }
        other => panic!("expected Ed25519, got {other:?}"),
    }

    let state = verify(
        &authority_grade_input(Kind::ReleaseReadiness),
        &witnesses,
        now,
        Duration::seconds(86_400),
    );

    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, WITNESS_SIGNATURE_INVALID);
        }
        other => panic!("expected Broken(signature_invalid), got {other:?}"),
    }
}

#[test]
fn signature_invalid_when_payload_class_does_not_match_declared_class() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let mut witness = signed_witness(
        7,
        WitnessClass::SignedLedgerChainHead,
        WitnessTier::OperatorOwned,
        &digest,
        now,
    );
    // Swap the payload to a different class -- triggers signature reject because
    // the canonical preimage no longer matches what we signed.
    witness.payload = sample_payload(WitnessClass::ExternalAnchorCrossing);

    let state = verify(
        &authority_grade_input(Kind::ReleaseReadiness),
        &[witness],
        now,
        Duration::seconds(86_400),
    );

    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, WITNESS_SIGNATURE_INVALID);
        }
        other => panic!("expected Broken(signature_invalid), got {other:?}"),
    }
}

#[test]
fn ceiling_below_required_when_proof_state_partial() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let witnesses = full_witness_set(&digest, now);
    // FullChainVerified shape but downgrade the proof state so the effective
    // ceiling drops below the required ceiling for ReleaseReadiness.
    let input = EvidenceInput {
        kind: Kind::ReleaseReadiness,
        evidence_blake3: digest,
        runtime_mode: RuntimeMode::AuthorityGrade,
        authority_class: AuthorityClass::Operator,
        proof_state: ClaimProofState::Partial,
        requested_ceiling: ClaimCeiling::AuthorityGrade,
        source_refs: vec!["signed://x".into()],
        advisory_only: false,
    };

    let state = verify(&input, &witnesses, now, Duration::seconds(86_400));
    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, COMPOSITION_CEILING_BELOW_REQUIRED);
        }
        other => panic!("expected Broken(ceiling_below_required), got {other:?}"),
    }
}

#[test]
fn policy_reject_fails_closed_regardless_of_witnesses() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let witnesses = full_witness_set(&digest, now);
    let policy = PolicyDecision {
        final_outcome: PolicyOutcome::Reject,
        contributing: vec![PolicyContribution::new(
            "test.policy_reject",
            PolicyOutcome::Reject,
            "policy hard reject for the test",
        )
        .expect("contribution is constructible")],
        discarded: Vec::new(),
        break_glass: None,
    };

    let state = verify_with_policy(
        &authority_grade_input(Kind::ReleaseReadiness),
        &witnesses,
        now,
        Duration::seconds(86_400),
        Some(&policy),
    );

    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, COMPOSITION_POLICY_FAIL_CLOSED);
            assert!(edge.detail.contains("Reject"));
        }
        other => panic!("expected Broken(policy_fail_closed), got {other:?}"),
    }
}

#[test]
fn advisory_only_input_is_tier_insufficient_even_with_full_witnesses() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let witnesses = full_witness_set(&digest, now);

    let state = verify(
        &advisory_only_input(Kind::ReleaseReadiness),
        &witnesses,
        now,
        Duration::seconds(86_400),
    );

    match state {
        VerifiedTrustState::Broken { edge, .. } => {
            assert_eq!(edge.invariant, WITNESS_TIER_INSUFFICIENT);
            assert!(edge.detail.contains("advisory_only"));
        }
        other => {
            panic!("expected Broken(tier_insufficient) for advisory-only input, got {other:?}")
        }
    }
}

#[test]
fn full_witness_set_is_full_chain_verified() {
    let digest = sample_evidence_digest();
    let now = Utc::now();
    let witnesses = full_witness_set(&digest, now);

    let state = verify(
        &authority_grade_input(Kind::ReleaseReadiness),
        &witnesses,
        now,
        Duration::seconds(86_400),
    );

    assert!(state.is_full_chain_verified(), "got {state:?}");
}
