//! Integration tests for the lifted `RunEvidence` + `AttestContract` types.
//!
//! Provenance: tests are ported from `algol/tests/contract_fixtures.rs` @
//! `6956cfd347cd8ce492231ba5aaa4952227d72689`, re-licensed `AGPL-3.0-or-later`
//! per ec `draft-algol-into-tsafe-merge.md`. Updated in Phase 4 to cover
//! the BLAKE3 hash family + schema rename + v1.x compat-window
//! deserialization. See `CHANGELOG.md`.
//!
//! These exercise the public API as a downstream consumer would: import the
//! types via `tsafe_core::run_evidence::*` + `tsafe_core::attest_contract::*`,
//! construct artifacts, serialize/deserialize, and verify validation.

use chrono::Utc;
use tsafe_core::attest_contract::{
    AttestAllowedEnv, AttestContract, AttestContractPolicy, AttestDeniedEnv,
    ATTEST_CONTRACT_SCHEMA, LEGACY_ATTEST_CONTRACT_SCHEMA, REDACTION_BLAKE3,
    REDACTION_SHA256_LEGACY,
};
use tsafe_core::run_evidence::{
    blake3_hash, ContractRef, DeniedSensitiveEnvEvidence, EnforcementResult, EnvironmentEvidence,
    InjectedSecretEvidence, MachineEvidence, ProcessEvidence, RiskDelta, RunEvidence,
    LEGACY_RUN_SCHEMA, RUN_EVIDENCE_VERSION, RUN_SCHEMA,
};

fn well_formed_run_evidence() -> 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: Some("0123456789abcdef0123456789abcdef01234567".to_string()),
        command: vec!["true".to_string()],
        contract: ContractRef {
            path: "tsafe.attest.contract.json".to_string(),
            hash: blake3_hash(b"contract-bytes"),
        },
        environment: EnvironmentEvidence {
            parent_env_count: 5,
            child_env_count: 2,
            removed_env_count: 3,
            safe_baseline_injected: vec!["PATH".to_string(), "HOME".to_string()],
            secrets_injected: vec![InjectedSecretEvidence {
                name: "DATABASE_URL".to_string(),
                source: "literal://demo/DATABASE_URL".to_string(),
                hash: blake3_hash(b"postgres://example.invalid/app"),
                redacted_value: "post****/app".to_string(),
                required: true,
            }],
            sensitive_env_denied: vec![DeniedSensitiveEnvEvidence {
                name: "AWS_SECRET_ACCESS_KEY".to_string(),
                hash: blake3_hash(b"ambient-aws-secret-from-parent"),
                reason: "sensitive parent-env entry denied by policy".to_string(),
            }],
        },
        process: ProcessEvidence {
            pid: 1234,
            exit_code: 0,
            duration_ms: 42,
            cwd: "/tmp/test-repo".to_string(),
        },
        machine: MachineEvidence {
            hostname_hash: blake3_hash(b"test-host"),
            username_hash: blake3_hash(b"test-user"),
            os: "linux".to_string(),
            arch: "x86_64".to_string(),
        },
        result: EnforcementResult {
            contract_enforced: true,
            violations: Vec::new(),
            risk_delta: RiskDelta {
                before_score: 30,
                after_score: 0,
            },
        },
        signature: None,
    }
}

fn well_formed_attest_contract() -> AttestContract {
    AttestContract {
        schema: ATTEST_CONTRACT_SCHEMA.to_string(),
        repo_path: "/tmp/test-repo".to_string(),
        repo_commit: None,
        created_at: Utc::now(),
        created_by: "integration-test".to_string(),
        command: vec!["sh".to_string(), "-c".to_string(), "true".to_string()],
        policy: AttestContractPolicy {
            default_env: "deny".to_string(),
            allow_safe_baseline: true,
            redaction: REDACTION_BLAKE3.to_string(),
        },
        allowed_env: vec![AttestAllowedEnv {
            name: "DATABASE_URL".to_string(),
            source: "env://DATABASE_URL".to_string(),
            required: true,
            reason: "the test command reads DATABASE_URL".to_string(),
        }],
        denied_env: vec![AttestDeniedEnv {
            name: "AWS_SECRET_ACCESS_KEY".to_string(),
            reason: "AWS credentials never needed for this command".to_string(),
        }],
        safe_baseline_env: vec!["PATH".to_string(), "HOME".to_string()],
        source_scan: None,
    }
}

#[test]
fn run_evidence_serialises_and_round_trips_through_json() {
    let original = well_formed_run_evidence();
    let json = serde_json::to_string(&original).expect("serialise RunEvidence");
    let parsed: RunEvidence = serde_json::from_str(&json).expect("deserialise RunEvidence");
    assert_eq!(original, parsed, "JSON round-trip changed the artifact");
}

#[test]
fn run_evidence_serialises_with_expected_top_level_keys() {
    let evidence = well_formed_run_evidence();
    let value: serde_json::Value = serde_json::to_value(&evidence).expect("to_value");
    for key in [
        "schema",
        // Phase 4 rename: the new canonical wire key is `tsafe_attest_version`.
        "tsafe_attest_version",
        "started_at",
        "finished_at",
        "repo_path",
        "repo_commit",
        "command",
        "contract",
        "environment",
        "process",
        "machine",
        "result",
    ] {
        assert!(
            value.get(key).is_some(),
            "RunEvidence JSON missing required key {key}; value = {value}"
        );
    }
}

#[test]
fn run_evidence_environment_contains_per_var_blake3_hashes() {
    let evidence = well_formed_run_evidence();
    // Every secret injected and every sensitive entry denied carries a
    // blake3-prefixed hash — the Phase 4 BLAKE3-convergence invariant.
    for secret in &evidence.environment.secrets_injected {
        assert!(
            secret.hash.starts_with("blake3:"),
            "secrets_injected[{}].hash should be blake3:-prefixed (Phase 4): {}",
            secret.name,
            secret.hash
        );
        assert_eq!(
            secret.hash.len(),
            "blake3:".len() + 64,
            "secrets_injected[{}].hash should be 64 hex chars after prefix: {}",
            secret.name,
            secret.hash
        );
    }
    for denied in &evidence.environment.sensitive_env_denied {
        assert!(
            denied.hash.starts_with("blake3:"),
            "sensitive_env_denied[{}].hash should be blake3:-prefixed: {}",
            denied.name,
            denied.hash
        );
    }
    // The machine hashes must also be blake3-prefixed.
    assert!(evidence.machine.hostname_hash.starts_with("blake3:"));
    assert!(evidence.machine.username_hash.starts_with("blake3:"));
    // And the contract pointer.
    assert!(evidence.contract.hash.starts_with("blake3:"));
}

#[test]
fn run_evidence_env_diff_invariant_holds_for_well_formed_artifact() {
    let evidence = well_formed_run_evidence();
    let parent = evidence.environment.parent_env_count;
    let child = evidence.environment.child_env_count;
    let removed = evidence.environment.removed_env_count;
    // The diff invariant: removed must equal (parent - child) AND
    // child must not exceed parent.
    assert!(child <= parent);
    assert_eq!(removed, parent - child);
    // Validation must accept this.
    assert!(
        evidence.ensure_valid().is_ok(),
        "well-formed evidence rejected: {:?}",
        evidence.validation_errors()
    );
}

#[test]
fn run_evidence_validation_rejects_diff_invariant_violations() {
    let mut bad = well_formed_run_evidence();
    bad.environment.child_env_count = bad.environment.parent_env_count + 1;
    bad.environment.removed_env_count = 0;
    let errors = bad.validation_errors().join("; ");
    assert!(errors.contains("child_env_count must not exceed parent_env_count"));

    let mut wrong_removed = well_formed_run_evidence();
    wrong_removed.environment.removed_env_count = wrong_removed.environment.parent_env_count + 100;
    let errors = wrong_removed.validation_errors().join("; ");
    assert!(errors.contains("removed_env_count must equal"));
}

#[test]
fn run_evidence_validation_rejects_malformed_hashes() {
    let mut bad = well_formed_run_evidence();
    bad.contract.hash = "blake3:deadbeef".to_string();
    let errors = bad.validation_errors().join("; ");
    assert!(
        errors.contains("contract.hash must be a blake3 hash"),
        "expected blake3 rejection, got: {errors}"
    );

    let mut bad_hostname = well_formed_run_evidence();
    bad_hostname.machine.hostname_hash = "not-a-hash".to_string();
    let errors = bad_hostname.validation_errors().join("; ");
    assert!(errors.contains("machine.hostname_hash must be a blake3 hash"));
}

#[test]
fn run_evidence_compat_accepts_legacy_schema_and_sha256_hashes() {
    #[allow(deprecated)]
    use tsafe_core::run_evidence::sha256_hash;
    let mut evidence = well_formed_run_evidence();
    evidence.schema = LEGACY_RUN_SCHEMA.to_string();
    #[allow(deprecated)]
    {
        evidence.contract.hash = sha256_hash(b"contract-bytes");
        for item in &mut evidence.environment.secrets_injected {
            item.hash = sha256_hash(item.name.as_bytes());
        }
        for item in &mut evidence.environment.sensitive_env_denied {
            item.hash = sha256_hash(item.name.as_bytes());
        }
        evidence.machine.hostname_hash = sha256_hash(b"test-host");
        evidence.machine.username_hash = sha256_hash(b"test-user");
    }
    assert!(
        evidence.ensure_valid().is_ok(),
        "compat-window legacy artifact rejected: {:?}",
        evidence.validation_errors()
    );
}

#[test]
fn attest_contract_serialises_and_round_trips_through_json() {
    let original = well_formed_attest_contract();
    let json = serde_json::to_string(&original).expect("serialise AttestContract");
    let parsed: AttestContract = serde_json::from_str(&json).expect("deserialise AttestContract");
    assert_eq!(original, parsed, "JSON round-trip changed the artifact");
}

#[test]
fn attest_contract_serialises_with_expected_top_level_keys() {
    let contract = well_formed_attest_contract();
    let value: serde_json::Value = serde_json::to_value(&contract).expect("to_value");
    for key in [
        "schema",
        "repo_path",
        "repo_commit",
        "created_at",
        "created_by",
        "command",
        "policy",
        "allowed_env",
        "denied_env",
        "safe_baseline_env",
        "source_scan",
    ] {
        assert!(
            value.get(key).is_some(),
            "AttestContract JSON missing required key {key}; value = {value}"
        );
    }
}

#[test]
fn attest_contract_compat_accepts_legacy_schema_and_sha256_redaction() {
    let mut contract = well_formed_attest_contract();
    contract.schema = LEGACY_ATTEST_CONTRACT_SCHEMA.to_string();
    contract.policy.redaction = REDACTION_SHA256_LEGACY.to_string();
    assert!(
        contract.ensure_valid().is_ok(),
        "compat-window legacy contract rejected: {:?}",
        contract.validation_errors()
    );
}

#[test]
fn attest_contract_and_authority_contract_are_distinct_types() {
    // Compile-time check: this file uses both type names by their full path
    // and they must coexist as distinct types per ec Phase 0 audit §1.5.1.
    // If a future refactor merges them this test will fail to compile.
    use tsafe_core::attest_contract::AttestContract;
    use tsafe_core::contracts::AuthorityContract as VaultPolicyContract;

    // Establish they are observably distinct by listing the disjoint
    // field sets. We cannot Default-construct either, so this test just
    // proves the symbols resolve to different items.
    let _ = std::any::TypeId::of::<AttestContract>();
    let _ = std::any::TypeId::of::<VaultPolicyContract>();
    assert_ne!(
        std::any::TypeId::of::<AttestContract>(),
        std::any::TypeId::of::<VaultPolicyContract>(),
        "AttestContract and AuthorityContract must remain distinct types"
    );
}

#[test]
fn blake3_hash_helper_matches_validator() {
    // The helper and validator must agree: every hash emitted by
    // blake3_hash() must pass is_blake3_hash().
    use tsafe_core::run_evidence::is_blake3_hash;

    for payload in [
        b"".as_slice(),
        b"a".as_slice(),
        b"the quick brown fox jumps over the lazy dog".as_slice(),
        &[0u8; 1024],
    ] {
        let hash = blake3_hash(payload);
        assert!(
            is_blake3_hash(&hash),
            "blake3_hash output rejected by is_blake3_hash: {hash}"
        );
    }
}
