use super::*;

fn team(id: &str) -> ServingIdentity {
    ServingIdentity::Team(id.to_owned())
}
fn dkey(fp: &str) -> ServingIdentity {
    ServingIdentity::DeploymentKey {
        fingerprint: fp.to_owned(),
    }
}

/// The authoritative signed verdict wins over the marker BOTH ways: `Compromised`
/// refuses where the marker alone would pass, `Trusted` proceeds over marker tamper.
#[test]
fn signed_verdict_overrides_marker_both_ways() {
    use crate::signed_policy::SignedVerdict;
    let dir = tempfile::tempdir().unwrap();
    let home = dir.path();
    // Marker: opted-in, served requirements now MISSING on disk (marker alone would refuse).
    let cache = ManagedConfigCache {
        principal: Some("team-007".into()),
        had_requirements: true,
        fail_closed: true,
        ..Default::default()
    };
    // Signed says NOT compromised → proceed, overriding the marker's tamper signal.
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::Trusted,
        false,
        Some(&cache),
        home,
        &team("team-007")
    ));
    // Signed says compromised → refuse, though this intact marker alone would pass.
    let intact = ManagedConfigCache {
        principal: Some("team-007".into()),
        fail_closed: true,
        ..Default::default()
    };
    assert!(managed_policy_compromised_decision(
        SignedVerdict::Compromised,
        false,
        Some(&intact),
        home,
        &team("team-007")
    ));
}

/// `Trusted` must NOT short-circuit past the deploy-key fingerprint check (the signature
/// can't attest the local key): a mismatch falls through to the marker decision, while
/// `Compromised` refuses regardless of the fingerprint.
#[test]
fn signed_verdict_does_not_skip_deploy_key_fingerprint() {
    use crate::signed_policy::SignedVerdict;
    let dir = tempfile::tempdir().unwrap();
    let home = dir.path();
    // Marker recorded fingerprint "fp-cache"; the host is now configured with "fp-local".
    let opted_in = ManagedConfigCache {
        principal: Some("dep-1".into()),
        key_fingerprint: Some("fp-cache".into()),
        fail_closed: true,
        ..Default::default()
    };
    // Trusted + fingerprint mismatch forces the marker path, which refuses an
    // opted-in cache.
    assert!(managed_policy_compromised_decision(
        SignedVerdict::Trusted,
        true, // deploy-key fingerprint mismatch
        Some(&opted_in),
        home,
        &dkey("fp-local")
    ));
    // A matching fingerprint trusts the signed verdict as before.
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::Trusted,
        false,
        Some(&opted_in),
        home,
        &dkey("fp-cache")
    ));
    // Opted-OUT deploy host with a key change → not refused (preserves non-fail-closed behavior).
    let opted_out = ManagedConfigCache {
        principal: Some("dep-1".into()),
        key_fingerprint: Some("fp-cache".into()),
        fail_closed: false,
        ..Default::default()
    };
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::Trusted,
        true,
        Some(&opted_out),
        home,
        &dkey("fp-local")
    ));
    // Compromised refuses EVEN with a fingerprint mismatch — never falls through to
    // this opted-OUT marker.
    assert!(managed_policy_compromised_decision(
        SignedVerdict::Compromised,
        true,
        Some(&opted_out),
        home,
        &dkey("fp-local")
    ));
}

/// A sidecar read BLIP is not absence: unlike NoAuthenticSidecar it never refuses on
/// its own — the marker decision stands.
#[test]
fn unreadable_sidecar_falls_back_to_marker() {
    use crate::signed_policy::SignedVerdict;
    let dir = tempfile::tempdir().unwrap();
    let home = dir.path();
    // Served artifact INTACT: the same marker refuses under NoAuthenticSidecar
    // (pinned below) but must ALLOW under a mere read blip.
    std::fs::write(home.join("requirements.toml"), "[features]\n").unwrap();
    let served_fail_closed = ManagedConfigCache {
        principal: Some("team-007".into()),
        had_requirements: true,
        fail_closed: true,
        ..Default::default()
    };
    assert!(
        !managed_policy_compromised_decision(
            SignedVerdict::SidecarUnreadable,
            false,
            Some(&served_fail_closed),
            home,
            &team("team-007")
        ),
        "a transient sidecar read blip must not refuse a session"
    );
    // Marker-grade tamper (served artifact missing on disk) still refuses.
    std::fs::remove_file(home.join("requirements.toml")).unwrap();
    assert!(managed_policy_compromised_decision(
        SignedVerdict::SidecarUnreadable,
        false,
        Some(&served_fail_closed),
        home,
        &team("team-007")
    ));
}

/// NoAuthenticSidecar under a fail-closed marker that recorded served policy refuses —
/// stripping the sidecar must not downgrade enforcement to the forgeable marker path.
/// A marker that served nothing, never opted in, or is absent keeps the marker decision.
#[test]
fn missing_sidecar_under_fail_closed_marker_refuses() {
    use crate::signed_policy::SignedVerdict;
    let dir = tempfile::tempdir().unwrap();
    let home = dir.path();
    // The served artifact is INTACT on disk — the marker path alone would allow.
    std::fs::write(home.join("requirements.toml"), "[features]\n").unwrap();
    let served_fail_closed = ManagedConfigCache {
        principal: Some("team-007".into()),
        had_requirements: true,
        fail_closed: true,
        ..Default::default()
    };
    assert!(
        managed_policy_compromised_decision(
            SignedVerdict::NoAuthenticSidecar,
            false,
            Some(&served_fail_closed),
            home,
            &team("team-007")
        ),
        "a fail-closed marker with served policy requires an authentic sidecar"
    );
    // Served nothing → nothing the sidecar must cover → marker decision (allows).
    let served_nothing = ManagedConfigCache {
        principal: Some("team-007".into()),
        fail_closed: true,
        ..Default::default()
    };
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::NoAuthenticSidecar,
        false,
        Some(&served_nothing),
        home,
        &team("team-007")
    ));
    // Never opted in → marker decision (allows).
    let opted_out = ManagedConfigCache {
        principal: Some("team-007".into()),
        had_requirements: true,
        fail_closed: false,
        ..Default::default()
    };
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::NoAuthenticSidecar,
        false,
        Some(&opted_out),
        home,
        &team("team-007")
    ));
    // No marker at all → nothing to enforce.
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::NoAuthenticSidecar,
        false,
        None,
        home,
        &team("team-007")
    ));
}

/// The dark build (`Inactive`) falls through to the best-effort marker: opted-in +
/// a served artifact now missing refuses; opted-out or no marker proceeds.
#[test]
fn inactive_verdict_falls_through_to_marker() {
    use crate::signed_policy::SignedVerdict;
    let dir = tempfile::tempdir().unwrap();
    let home = dir.path();
    // Opted-in, recorded a requirements artifact, which is absent on disk → compromised.
    let missing = ManagedConfigCache {
        principal: Some("team-007".into()),
        had_requirements: true,
        fail_closed: true,
        ..Default::default()
    };
    assert!(managed_policy_compromised_decision(
        SignedVerdict::Inactive,
        false,
        Some(&missing),
        home,
        &team("team-007")
    ));
    // Opted-OUT marker → never refuses, even with a missing artifact.
    let optout = ManagedConfigCache {
        principal: Some("team-007".into()),
        had_requirements: true,
        fail_closed: false,
        ..Default::default()
    };
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::Inactive,
        false,
        Some(&optout),
        home,
        &team("team-007")
    ));
    // No marker at all → nothing to enforce.
    assert!(!managed_policy_compromised_decision(
        SignedVerdict::Inactive,
        false,
        None,
        home,
        &team("team-007")
    ));
}

#[test]
fn managed_config_stale_at_is_false_without_user_home() {
    // No user home => nothing to refresh into => not stale (prevents a
    // perpetual sync loop).
    assert!(!managed_config_stale_at(None, &ServingIdentity::None));
}

#[test]
fn managed_config_stale_at_is_true_without_synced_marker() {
    let dir = std::env::temp_dir().join(format!("kigi-stale-nomark-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let _ = std::fs::remove_file(dir.join(MANAGED_CONFIG_CACHE_FILE));
    // No recorded sync (even if config files exist) => stale.
    assert!(managed_config_stale_at(Some(&dir), &ServingIdentity::None));
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn managed_config_stale_at_is_false_after_fresh_sync() {
    let dir = std::env::temp_dir().join(format!("kigi-stale-fresh-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: None,
            had_managed_config: false,
            had_requirements: false,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    // A just-recorded sync is within the default 30-minute threshold.
    assert!(!managed_config_stale_at(Some(&dir), &ServingIdentity::None));
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn managed_deployment_id_at_requires_matching_fingerprint() {
    let dir = std::env::temp_dir().join(format!("kigi-dep-id-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let server_dep = "37c96487-eda9-4bb2-a767-6444274423c8";
    // Deploy-key path: fingerprint set, principal = server deployment UUID.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some(server_dep),
            had_managed_config: true,
            had_requirements: false,
            key_fingerprint: Some("fp-abc"),
            fail_closed: false,
        },
    );
    assert_eq!(
        super::managed_deployment_id_at(&dir, "fp-abc").as_deref(),
        Some(server_dep)
    );
    // Rotated key: recorded fingerprint no longer matches — stale principal must not leak.
    assert_eq!(super::managed_deployment_id_at(&dir, "fp-rotated"), None);
    assert_eq!(super::managed_deployment_id_at(&dir, ""), None);
    // Team path: fingerprint absent, principal is a team id — not a deployment UUID.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-xyz"),
            had_managed_config: true,
            had_requirements: false,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    assert_eq!(super::managed_deployment_id_at(&dir, "fp-abc"), None);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn managed_config_stale_at_is_true_for_old_sync() {
    let dir = std::env::temp_dir().join(format!("kigi-stale-old-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let hour_ago = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs()
        - 60 * 60;
    std::fs::write(
        dir.join(MANAGED_CONFIG_CACHE_FILE),
        format!("{{\"synced_at\":{hour_ago}}}"),
    )
    .unwrap();
    // An hour-old sync exceeds the default 30-minute threshold.
    assert!(managed_config_stale_at(Some(&dir), &ServingIdentity::None));
    let _ = std::fs::remove_dir_all(&dir);
}

/// A served-then-deleted artifact reads stale regardless of the timer.
#[test]
fn managed_config_stale_when_served_artifact_deleted() {
    let dir = std::env::temp_dir().join(format!("kigi-stale-artgone-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-1"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    assert!(!managed_config_stale_at(Some(&dir), &team("team-1")));
    std::fs::remove_file(dir.join("requirements.toml")).unwrap();
    assert!(managed_config_stale_at(Some(&dir), &team("team-1")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// A config-less principal that served nothing is never misread as stale.
#[test]
fn managed_config_not_stale_when_nothing_served() {
    let dir = std::env::temp_dir().join(format!("kigi-stale-noart-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-1"),
            had_managed_config: false,
            had_requirements: false,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    assert!(!managed_config_stale_at(Some(&dir), &team("team-1")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// A cache fetched for a different principal is stale for the current one.
#[test]
fn managed_config_stale_on_identity_mismatch() {
    let dir = std::env::temp_dir().join(format!("kigi-stale-ident-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: false,
            had_requirements: false,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    // Same identity => fresh; different identity => stale.
    assert!(!managed_config_stale_at(Some(&dir), &team("team-a")));
    assert!(managed_config_stale_at(Some(&dir), &team("team-b")));
    // Unknown current identity (None) never forces a refetch on identity.
    assert!(!managed_config_stale_at(Some(&dir), &ServingIdentity::None));
    let _ = std::fs::remove_dir_all(&dir);
}

/// Legacy marker (no `had_*`) is never flagged missing-artifact-stale.
#[test]
fn managed_config_legacy_marker_is_conservative() {
    let dir = std::env::temp_dir().join(format!("kigi-stale-legacy-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    std::fs::write(
        dir.join(MANAGED_CONFIG_CACHE_FILE),
        format!("{{\"synced_at\":{now}}}"),
    )
    .unwrap();
    assert!(!managed_config_stale_at(Some(&dir), &ServingIdentity::None));
    // A legacy marker (no principal) reads stale once via identity mismatch, so it self-upgrades next sync.
    assert!(managed_config_stale_at(Some(&dir), &team("team-x")));
    assert!(is_managed_config_hard_stale_for_at(&dir, &team("team-x")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// Hard-staleness: missing artifact or identity mismatch → true; a fresh same-identity cache → false.
#[test]
fn hard_stale_only_on_missing_or_identity() {
    let dir = std::env::temp_dir().join(format!("kigi-hardstale-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    // Same identity, artifact present → not hard-stale (even past the timer).
    assert!(!is_managed_config_hard_stale_for_at(&dir, &team("team-a")));
    // Different identity → hard-stale.
    assert!(is_managed_config_hard_stale_for_at(&dir, &team("team-b")));
    // Served artifact deleted → hard-stale.
    std::fs::remove_file(dir.join("requirements.toml")).unwrap();
    assert!(is_managed_config_hard_stale_for_at(&dir, &team("team-a")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// No marker → hard-stale (never synced → fetch before use).
#[test]
fn hard_stale_without_marker() {
    let dir = std::env::temp_dir().join(format!("kigi-hardstale-nomark-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let _ = std::fs::remove_file(dir.join(MANAGED_CONFIG_CACHE_FILE));
    assert!(is_managed_config_hard_stale_for_at(&dir, &team("team-a")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// A corrupt marker reads as "no marker": the gate ALLOWS (corruption or a torn write
/// must not lock a managed user out) and the cache is hard-stale so the next sync rewrites it.
#[test]
fn corrupt_marker_reads_as_no_marker_and_allows() {
    let dir = std::env::temp_dir().join(format!("kigi-corrupt-marker-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("requirements.toml"), "fail_closed = true\n").unwrap();
    std::fs::write(dir.join(MANAGED_CONFIG_CACHE_FILE), "{ not valid json").unwrap();

    assert!(read_managed_config_cache(&dir).is_none());
    // No usable marker → not compromised, so corruption can't lock a managed user out...
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-a")));
    // ...but the cache reads hard-stale, so the next sync refetches and rewrites the marker.
    assert!(is_managed_config_hard_stale_for_at(&dir, &team("team-a")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// A deploy-key switch is detected offline as an identity mismatch (`cache_unusable_for`) and refetched online.
#[test]
fn deployment_key_switch_is_stale_and_tampered_offline() {
    let dir = std::env::temp_dir().join(format!("kigi-dk-switch-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    // Provisioned with key A: principal = served deployment_id, fingerprint = fp-a.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-A"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: Some("fp-a"),
            fail_closed: false,
        },
    );
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();

    // Same key, artifacts intact → not stale, not tampered.
    assert!(!is_managed_config_hard_stale_for_at(&dir, &dkey("fp-a")));
    assert!(!managed_config_stale_at(Some(&dir), &dkey("fp-a")));

    // Different key (fp-b) → identity mismatch: hard-stale and tampered.
    assert!(is_managed_config_hard_stale_for_at(&dir, &dkey("fp-b")));
    assert!(managed_config_stale_at(Some(&dir), &dkey("fp-b")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// A pre-upgrade marker (no `key_fingerprint`) must not fire when a key is now configured — it self-upgrades next sync.
#[test]
fn pre_upgrade_marker_without_fingerprint_does_not_fire_on_key() {
    let dir = std::env::temp_dir().join(format!("kigi-dk-preupgrade-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    // Legacy marker: synced, an artifact served, but no key_fingerprint field.
    std::fs::write(
        dir.join(MANAGED_CONFIG_CACHE_FILE),
        format!("{{\"synced_at\":{now},\"had_requirements\":true}}"),
    )
    .unwrap();
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    // A key is configured now but the marker has none → no key mismatch, no spurious refetch.
    assert!(!is_managed_config_hard_stale_for_at(
        &dir,
        &dkey("fp-current")
    ));
    assert!(!managed_config_stale_at(Some(&dir), &dkey("fp-current")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// The team path keys on `principal` (team id), records no fingerprint, and never fires a key mismatch.
#[test]
fn team_path_keys_on_principal_not_key_fingerprint() {
    let dir = std::env::temp_dir().join(format!("kigi-team-nofp-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    // Team path: identity carries no fingerprint → never a key mismatch.
    assert!(!is_managed_config_hard_stale_for_at(&dir, &team("team-a")));
    // A team switch is still detected via principal (unchanged behavior).
    assert!(is_managed_config_hard_stale_for_at(&dir, &team("team-b")));
    // No key fingerprint is recorded on the team path.
    let marker = std::fs::read_to_string(dir.join(MANAGED_CONFIG_CACHE_FILE)).unwrap();
    let v: serde_json::Value = serde_json::from_str(&marker).unwrap();
    assert!(
        v["key_fingerprint"].is_null(),
        "team path must not record a key fingerprint: {marker}"
    );
    let _ = std::fs::remove_dir_all(&dir);
}

/// The eviction trigger fires only on a confirmed switch; first sync, same identity, `None`, and pre-upgrade markers never fire.
#[test]
fn identity_changed_only_on_confirmed_switch() {
    let dir = std::env::temp_dir().join(format!("kigi-ident-changed-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();

    // No marker yet → first sync, nothing to evict.
    assert!(!managed_config_identity_changed_at(
        &dir,
        Some("team-a"),
        None
    ));

    // Team marker: same team → no switch; different team → switch; unknown (None) → never evicts.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: true,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    assert!(!managed_config_identity_changed_at(
        &dir,
        Some("team-a"),
        None
    ));
    assert!(managed_config_identity_changed_at(
        &dir,
        Some("team-b"),
        None
    ));
    assert!(!managed_config_identity_changed_at(&dir, None, None));

    // Deploy-key marker: same fingerprint → no switch; changed → switch (even if only the fingerprint differs).
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-a"),
            had_managed_config: true,
            had_requirements: true,
            key_fingerprint: Some("fp-a"),
            fail_closed: false,
        },
    );
    assert!(!managed_config_identity_changed_at(
        &dir,
        Some("dep-a"),
        Some("fp-a")
    ));
    assert!(managed_config_identity_changed_at(
        &dir,
        Some("dep-b"),
        Some("fp-b")
    ));
    assert!(managed_config_identity_changed_at(&dir, None, Some("fp-b")));

    // Pre-upgrade team marker (no fingerprint) + a now-configured key → not a switch (key dimension has no recorded side).
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: true,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    assert!(!managed_config_identity_changed_at(
        &dir,
        Some("team-a"),
        Some("fp-current")
    ));

    let _ = std::fs::remove_dir_all(&dir);
}

/// A blank/whitespace value is "unknown", never a distinct identity — on EITHER side of EITHER
/// dimension (principal or key fingerprint): a malformed `auth.json` or corrupt marker must not
/// make the gate purge / apply eviction shed a real tenant's policy.
#[test]
fn blank_principal_is_never_a_confirmed_switch() {
    let dir = std::env::temp_dir().join(format!("kigi-ident-blank-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();

    // Real recorded team, blank current → not a switch.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: true,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    for blank in ["", "   "] {
        assert!(
            !managed_config_identity_changed_at(&dir, Some(blank), None),
            "a blank current principal ({blank:?}) must not read as a confirmed switch"
        );
    }

    // Blank recorded principal, real current → not a switch either.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("  "),
            had_managed_config: true,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    assert!(
        !managed_config_identity_changed_at(&dir, Some("team-b"), None),
        "a blank recorded principal must not read as a distinct identity"
    );

    // Fingerprint dimension, same symmetry: a blank side never confirms; two real,
    // differing fingerprints still do.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-a"),
            had_managed_config: true,
            had_requirements: true,
            key_fingerprint: Some("  "),
            fail_closed: false,
        },
    );
    assert!(
        !managed_config_identity_changed_at(&dir, Some("dep-a"), Some("fp-b")),
        "a blank recorded fingerprint must not read as a distinct identity"
    );
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-a"),
            had_managed_config: true,
            had_requirements: true,
            key_fingerprint: Some("fp-a"),
            fail_closed: false,
        },
    );
    assert!(
        !managed_config_identity_changed_at(&dir, Some("dep-a"), Some("")),
        "a blank current fingerprint must not read as a confirmed switch"
    );
    assert!(
        managed_config_identity_changed_at(&dir, Some("dep-a"), Some("fp-b")),
        "two real, differing fingerprints must still confirm a switch"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

/// Compromised only when opted in AND tampered; opted-out / never-synced / config-less / intact is never flagged.
#[test]
fn compromised_only_when_opted_in_and_deleted_or_substituted() {
    let dir = std::env::temp_dir().join(format!("kigi-compromised-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();

    // No marker → not compromised.
    let _ = std::fs::remove_file(dir.join(MANAGED_CONFIG_CACHE_FILE));
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-a")));

    // Opted-in + present + same identity → not compromised.
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: true,
        },
    );
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-a")));

    // Served-then-deleted (admin opted in) → compromised.
    std::fs::remove_file(dir.join("requirements.toml")).unwrap();
    assert!(managed_policy_compromised_for_at(&dir, &team("team-a")));

    // Different principal, artifact still missing → compromised by the artifact, not the identity.
    assert!(managed_policy_compromised_for_at(&dir, &team("team-b")));

    // Not opted in → a deletion is NOT failed closed.
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    std::fs::remove_file(dir.join("requirements.toml")).unwrap();
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-a")));

    // Config-less principal (nothing served) → never compromised.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-c"),
            had_managed_config: false,
            had_requirements: false,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-c")));

    let _ = std::fs::remove_dir_all(&dir);
}

/// The OTHER served artifact — `managed_config.toml`, not just `requirements.toml` —
/// deleted under a fail_closed marker is compromised.
#[test]
fn compromised_on_managed_config_deletion_when_fail_closed() {
    let dir = std::env::temp_dir().join(format!("kigi-compromised-mc-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("managed_config.toml"), "[cli]\n").unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-a"),
            had_managed_config: true,
            had_requirements: false,
            key_fingerprint: None,
            fail_closed: true,
        },
    );
    // Present → not compromised.
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-a")));
    // Served-then-deleted managed_config.toml → compromised by the missing artifact.
    std::fs::remove_file(dir.join("managed_config.toml")).unwrap();
    assert!(managed_policy_compromised_for_at(&dir, &team("team-a")));
    let _ = std::fs::remove_dir_all(&dir);
}

/// Deployment-key path: an opted-in marker is compromised on an offline key switch (the fingerprint is the only offline identity).
#[test]
fn compromised_on_deployment_key_switch_when_fail_closed() {
    let dir = std::env::temp_dir().join(format!("kigi-compromised-dk-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();

    // Provisioned with key A (fp-a), opted into fail_closed, artifact present.
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-A"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: Some("fp-a"),
            fail_closed: true,
        },
    );

    // Same key, offline → not compromised (ALLOW).
    assert!(!managed_policy_compromised_for_at(&dir, &dkey("fp-a")));

    // Different key (fp-b) → compromised (REFUSE): offline deploy-key switch.
    assert!(managed_policy_compromised_for_at(&dir, &dkey("fp-b")));

    // Not opted in (fail_closed=false): a key switch is NOT failed closed.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-A"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: Some("fp-a"),
            fail_closed: false,
        },
    );
    assert!(!managed_policy_compromised_for_at(&dir, &dkey("fp-b")));

    let _ = std::fs::remove_dir_all(&dir);
}

/// The gate refuses ONLY on tamper for the CURRENT principal (artifact missing or changed key fingerprint),
/// never a pure identity mismatch; staleness still treats that mismatch as a refetch trigger (asserted alongside).
#[test]
fn gate_excludes_pure_identity_mismatch_but_keeps_artifact_and_key_tamper() {
    let dir = std::env::temp_dir().join(format!("kigi-gate-fix1-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();

    // (1) Principal A (fail_closed), artifact intact; serving team-b = pure identity mismatch → ALLOWED.
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-A"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: true,
        },
    );
    assert!(
        !managed_policy_compromised_for_at(&dir, &team("team-b")),
        "a foreign/stale principal's fail_closed must NOT refuse the current session"
    );
    // ...but still stale for B → the refetch path rebinds online.
    assert!(
        is_managed_config_hard_stale_for_at(&dir, &team("team-b")),
        "a pure identity mismatch must still trigger a refetch (rebind)"
    );

    // (2) Same principal, served artifact now missing → still REFUSED offline.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-b"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: true,
        },
    );
    std::fs::remove_file(dir.join("requirements.toml")).unwrap();
    assert!(
        managed_policy_compromised_for_at(&dir, &team("team-b")),
        "same-principal served-then-deleted artifact must fail closed offline"
    );

    // (3) Deploy-key fingerprint mismatch for the current key → still REFUSED.
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("dep-A"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: Some("fp-a"),
            fail_closed: true,
        },
    );
    assert!(
        managed_policy_compromised_for_at(&dir, &dkey("fp-b")),
        "a changed deployment-key fingerprint must fail closed offline"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

/// Opt-in comes from the served response, not disk, so a no-write sync can't disarm the gate.
#[test]
fn mark_keeps_fail_closed_armed_without_on_disk_file() {
    let dir = std::env::temp_dir().join(format!("kigi-mark-disarm-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();

    // Opted-in policy served + present → not compromised (intact).
    std::fs::write(dir.join("requirements.toml"), "[features]\n").unwrap();
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-1"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: true,
        },
    );
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-1")));

    // Tamper: delete the served file → compromised.
    std::fs::remove_file(dir.join("requirements.toml")).unwrap();
    assert!(managed_policy_compromised_for_at(&dir, &team("team-1")));

    // A no-write sync (file still absent) stays armed: opt-in is from the response.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-1"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: true,
        },
    );
    assert!(
        managed_policy_compromised_for_at(&dir, &team("team-1")),
        "a no-write sync must not disarm the fail-closed gate"
    );

    // An opt-OUT (fail_closed=false) still takes effect.
    mark_managed_config_synced_at(
        &dir,
        SyncMarker {
            principal: Some("team-1"),
            had_managed_config: false,
            had_requirements: true,
            key_fingerprint: None,
            fail_closed: false,
        },
    );
    assert!(!managed_policy_compromised_for_at(&dir, &team("team-1")));

    let _ = std::fs::remove_dir_all(&dir);
}

/// The gate's apply-race retry: a Compromised refusal that clears on the second
/// evaluation (an in-flight apply settled) allows; real tamper stays refused; and
/// non-Compromised refusals never retry.
#[test]
fn gate_retries_once_on_a_compromised_verdict() {
    use crate::signed_policy::SignedVerdict;
    // Racing apply: mismatch on the first read, settled on the second → allowed.
    let mut calls = 0;
    let allowed = !compromised_with_apply_race_retry(
        || {
            calls += 1;
            match calls {
                1 => (true, SignedVerdict::Compromised),
                _ => (false, SignedVerdict::Trusted),
            }
        },
        || {},
    );
    assert!(allowed, "a settled racing write must not refuse");
    assert_eq!(calls, 2, "exactly one retry");

    // Real tamper: Compromised on both evaluations → still refused.
    assert!(compromised_with_apply_race_retry(
        || (true, SignedVerdict::Compromised),
        || {},
    ));

    // A non-Compromised refusal (e.g. a stripped sidecar) refuses without retrying.
    let mut evals = 0;
    let refused = compromised_with_apply_race_retry(
        || {
            evals += 1;
            (true, SignedVerdict::NoAuthenticSidecar)
        },
        || panic!("no pause for non-Compromised refusals"),
    );
    assert!(refused);
    assert_eq!(evals, 1);
}
