//! Phase 5 tamper-detection integration test.
//!
//! Verifies that any post-sign mutation of a `RunEvidence` artifact
//! causes [`tsafe_core::sign::verify_evidence`] to reject the signature.
//! This is the central security property the Phase 5 wire promises:
//! signatures bind authorship to the *exact* canonical encoding of the
//! artifact at sign time.
//!
//! Provenance: Phase 5 of the algol→tsafe merge per
//! `ecosystem-catalog/portfolio-algol-tsafe-roadmap-to-code-complete-2026-05-21.md`
//! §5.1.

use chrono::Utc;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use tsafe_core::run_evidence::{
    blake3_hash, ContractRef, DeniedSensitiveEnvEvidence, EnforcementResult, EnvironmentEvidence,
    InjectedSecretEvidence, MachineEvidence, ProcessEvidence, RiskDelta, RunEvidence,
    RUN_EVIDENCE_VERSION, RUN_SCHEMA,
};
use tsafe_core::sign::{sign_evidence, verify_evidence, SignedEvidence, VerifyError};

fn well_formed() -> RunEvidence {
    let now = Utc::now();
    RunEvidence {
        schema: RUN_SCHEMA.to_string(),
        tsafe_attest_version: RUN_EVIDENCE_VERSION.to_string(),
        started_at: now,
        finished_at: now,
        repo_path: "/tmp/test-repo".to_string(),
        repo_commit: None,
        command: vec!["true".to_string()],
        contract: ContractRef {
            path: "tsafe.contract.json".to_string(),
            hash: blake3_hash(b"contract"),
        },
        environment: EnvironmentEvidence {
            parent_env_count: 3,
            child_env_count: 1,
            removed_env_count: 2,
            safe_baseline_injected: vec!["PATH".to_string()],
            secrets_injected: vec![InjectedSecretEvidence {
                name: "DATABASE_URL".to_string(),
                source: "literal://demo/DATABASE_URL".to_string(),
                hash: blake3_hash(b"db"),
                redacted_value: "p***".to_string(),
                required: true,
            }],
            sensitive_env_denied: vec![DeniedSensitiveEnvEvidence {
                name: "AWS_SECRET_ACCESS_KEY".to_string(),
                hash: blake3_hash(b"aws"),
                reason: "test".to_string(),
            }],
        },
        process: ProcessEvidence {
            pid: 1234,
            exit_code: 0,
            duration_ms: 42,
            cwd: "/tmp".to_string(),
        },
        machine: MachineEvidence {
            hostname_hash: blake3_hash(b"host"),
            username_hash: blake3_hash(b"user"),
            os: "linux".to_string(),
            arch: "x86_64".to_string(),
        },
        result: EnforcementResult {
            contract_enforced: true,
            violations: Vec::new(),
            risk_delta: RiskDelta {
                before_score: 10,
                after_score: 0,
            },
        },
        signature: None,
    }
}

fn signing_key() -> SigningKey {
    SigningKey::generate(&mut OsRng)
}

/// Re-sign the artifact and return the produced `SignedEvidence` along
/// with the verifying key that should accept it.
fn fresh_signed() -> (SignedEvidence, ed25519_dalek::VerifyingKey) {
    let key = signing_key();
    let signed = sign_evidence(&well_formed(), &key).expect("sign");
    (signed, key.verifying_key())
}

fn assert_tampered(result: Result<(), VerifyError>) {
    match result {
        Err(VerifyError::SignatureMismatch(_)) => {}
        other => panic!("expected SignatureMismatch on tampered evidence, got {other:?}"),
    }
}

#[test]
fn tampered_exit_code_fails_verification() {
    let (mut signed, vk) = fresh_signed();
    signed.evidence.process.exit_code = 1;
    assert_tampered(verify_evidence(&signed, &vk));
}

#[test]
fn tampered_command_fails_verification() {
    let (mut signed, vk) = fresh_signed();
    signed.evidence.command = vec!["malicious".to_string()];
    assert_tampered(verify_evidence(&signed, &vk));
}

#[test]
fn tampered_contract_hash_fails_verification() {
    let (mut signed, vk) = fresh_signed();
    signed.evidence.contract.hash = blake3_hash(b"swapped-contract");
    assert_tampered(verify_evidence(&signed, &vk));
}

#[test]
fn tampered_injected_secret_hash_fails_verification() {
    let (mut signed, vk) = fresh_signed();
    if let Some(first) = signed.evidence.environment.secrets_injected.first_mut() {
        first.hash = blake3_hash(b"different-secret");
    }
    assert_tampered(verify_evidence(&signed, &vk));
}

#[test]
fn tampered_machine_hostname_fails_verification() {
    let (mut signed, vk) = fresh_signed();
    signed.evidence.machine.hostname_hash = blake3_hash(b"attacker-host");
    assert_tampered(verify_evidence(&signed, &vk));
}

#[test]
fn tampered_repo_path_fails_verification() {
    let (mut signed, vk) = fresh_signed();
    signed.evidence.repo_path = "/var/attacker/repo".to_string();
    assert_tampered(verify_evidence(&signed, &vk));
}

#[test]
fn tampered_risk_delta_fails_verification() {
    let (mut signed, vk) = fresh_signed();
    signed.evidence.result.risk_delta.after_score = 99;
    assert_tampered(verify_evidence(&signed, &vk));
}

#[test]
fn verifying_with_a_different_key_fails() {
    let (signed, _vk_correct) = fresh_signed();
    let wrong = signing_key().verifying_key();
    assert_tampered(verify_evidence(&signed, &wrong));
}

#[test]
fn unsupported_algorithm_is_rejected_before_crypto() {
    let (mut signed, vk) = fresh_signed();
    signed.signature.algo = "rsa-pss".to_string();
    if let Some(payload) = signed.evidence.signature.as_mut() {
        payload.algo = "rsa-pss".to_string();
    }
    match verify_evidence(&signed, &vk) {
        Err(VerifyError::UnsupportedAlgorithm(name)) => {
            assert_eq!(name, "rsa-pss");
        }
        other => panic!("expected UnsupportedAlgorithm, got {other:?}"),
    }
}

#[test]
fn truncated_signature_bytes_are_rejected() {
    let (mut signed, vk) = fresh_signed();
    // Drop a chunk off the end of the base64url signature, then verify.
    signed
        .signature
        .sig
        .truncate(signed.signature.sig.len() - 4);
    let result = verify_evidence(&signed, &vk);
    // Either SignatureLength (if truncation crossed a byte boundary) or
    // Base64 (if it produced an invalid base64 string) or
    // SignatureMismatch (if it landed on a value-but-wrong shape) is
    // acceptable — the only outcome we forbid is Ok.
    assert!(result.is_err(), "truncated signature must not verify");
}

#[test]
fn signature_field_missing_on_evidence_means_no_signed_evidence_can_be_constructed() {
    let evidence = well_formed();
    // The convenience constructor returns None for unsigned artifacts;
    // this is how a CLI surface knows to emit the "signature absent"
    // exit code (5) rather than calling verify_evidence at all.
    assert!(tsafe_core::sign::signed_from_run_evidence(evidence).is_none());
}
