use std::{
    collections::BTreeSet,
    fs,
    io::{BufRead, BufReader, Write},
    path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};

#[cfg(not(test))]
use crate::broker;
#[cfg(test)]
use crate::unlock;
use crate::{
    approval_receipts::{self, ApprovalReceipt},
    approvals::{ApprovalDecision, ApprovalScope, ApprovalSource},
    context, fs_util, logs,
    policy::AccessRequest,
};

const SESSION_GRANT_HOURS: i64 = 8;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalGrant {
    pub id: uuid::Uuid,
    pub created_at: DateTime<Utc>,
    pub expires_at: Option<DateTime<Utc>>,
    pub project: String,
    pub agent: Option<String>,
    pub branch: Option<String>,
    pub command: String,
    pub approved_env: Vec<String>,
    pub scope: ApprovalScope,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub uses_remaining: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub receipt: Option<ApprovalReceipt>,
}

#[derive(Debug, Clone)]
pub struct GrantReceiptContext {
    pub request_id: uuid::Uuid,
    pub critical_confirmation: bool,
    pub verified_context: Option<context::VerifiedContext>,
}

impl GrantReceiptContext {
    pub fn synthetic(critical_confirmation: bool) -> Self {
        Self {
            request_id: uuid::Uuid::new_v4(),
            critical_confirmation,
            verified_context: None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrantIntegrityStatus {
    Valid,
    Expired,
    LegacyUnsigned,
    Invalid,
}

pub fn grants_path() -> PathBuf {
    logs::ward_home().join("sessions").join("grants.jsonl")
}

pub fn find_matching_grant(access: &AccessRequest) -> Result<Option<ApprovalGrant>> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    Ok(find_matching_grant_in(&grants, access, Utc::now()).cloned())
}

pub fn find_matching_grant_with_context(
    access: &AccessRequest,
    verified_context: &context::VerifiedContext,
) -> Result<Option<ApprovalGrant>> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    Ok(find_matching_grant_in_with_context(&grants, access, Utc::now(), verified_context).cloned())
}

pub fn find_matching_once_grant(
    access: &AccessRequest,
    critical_required: bool,
) -> Result<Option<ApprovalGrant>> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    Ok(find_matching_once_grant_in(&grants, access, Utc::now(), critical_required).cloned())
}

pub fn find_matching_once_grant_with_context(
    access: &AccessRequest,
    critical_required: bool,
    verified_context: &context::VerifiedContext,
) -> Result<Option<ApprovalGrant>> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    Ok(find_matching_once_grant_in_with_context(
        &grants,
        access,
        Utc::now(),
        critical_required,
        verified_context,
    )
    .cloned())
}

pub fn find_matching_non_always_grant(access: &AccessRequest) -> Result<Option<ApprovalGrant>> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    Ok(find_matching_non_always_grant_in(&grants, access, Utc::now()).cloned())
}

pub fn find_matching_non_always_grant_with_context(
    access: &AccessRequest,
    verified_context: &context::VerifiedContext,
) -> Result<Option<ApprovalGrant>> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    Ok(find_matching_non_always_grant_in_with_context(
        &grants,
        access,
        Utc::now(),
        verified_context,
    )
    .cloned())
}

pub fn load_grants() -> Result<Vec<ApprovalGrant>> {
    load_grants_from_path(&grants_path())
}

pub fn persist_grant(
    access: &AccessRequest,
    decision: &ApprovalDecision,
    vault: &Path,
    receipt_context: Option<GrantReceiptContext>,
) -> Result<Option<ApprovalGrant>> {
    if !decision.approved
        || !decision.source.is_persistable_approval()
        || !decision.scope.is_persisted_grant()
    {
        return Ok(None);
    }

    let path = grants_path();
    let mut grant = grant_from_decision(access, decision, Utc::now())?;
    let receipt_context = match receipt_context {
        Some(context) => context,
        None => GrantReceiptContext::synthetic(false),
    };
    sign_grant(access, vault, &mut grant, receipt_context)?;
    append_grant_to_path(&path, &grant)?;
    Ok(Some(grant))
}

pub fn persist_manual_grant(
    access: &AccessRequest,
    scope: ApprovalScope,
    source: ApprovalSource,
    vault: &Path,
    receipt_context: Option<GrantReceiptContext>,
) -> Result<ApprovalGrant> {
    if !source.is_persistable_approval() {
        anyhow::bail!("{source:?} cannot create approval grants");
    }
    if scope == ApprovalScope::Deny {
        anyhow::bail!("deny cannot be persisted as an approval grant");
    }
    let decision = ApprovalDecision {
        approved: true,
        scope,
        approved_env: access.env.clone(),
        denied_env: Vec::new(),
        source,
        grant_id: None,
    };
    let mut grant = grant_from_decision(access, &decision, Utc::now())?;
    sign_grant(
        access,
        vault,
        &mut grant,
        receipt_context.unwrap_or_else(|| GrantReceiptContext::synthetic(false)),
    )?;
    append_grant_to_path(&grants_path(), &grant)?;
    Ok(grant)
}

pub fn approval_from_grant(access: &AccessRequest, grant: &ApprovalGrant) -> ApprovalDecision {
    ApprovalDecision {
        approved: true,
        scope: grant.scope,
        approved_env: access.env.clone(),
        denied_env: Vec::new(),
        source: ApprovalSource::Grant,
        grant_id: Some(grant.id),
    }
}

pub fn revoke_session_grants() -> Result<usize> {
    let path = grants_path();
    revoke_session_grants_at_path(&path)
}

pub fn revoke_grant(id: uuid::Uuid) -> Result<bool> {
    revoke_grant_at_path(&grants_path(), id)
}

pub fn prune_expired_grants() -> Result<usize> {
    prune_expired_grants_at_path(&grants_path(), Utc::now())
}

pub fn remove_project_grants(project: &str) -> Result<usize> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    let before = grants.len();
    let retained = grants
        .into_iter()
        .filter(|grant| grant.project != project)
        .collect::<Vec<_>>();
    let removed = before - retained.len();
    if removed > 0 {
        write_grants_to_path(&path, &retained)?;
    }
    Ok(removed)
}

pub fn consume_once_grant(id: uuid::Uuid) -> Result<bool> {
    let path = grants_path();
    let grants = load_grants_from_path(&path)?;
    let before = grants.len();
    let retained = grants
        .into_iter()
        .filter(|grant| !(grant.id == id && grant.scope == ApprovalScope::Once))
        .collect::<Vec<_>>();
    if retained.len() == before {
        return Ok(false);
    }
    write_grants_to_path(&path, &retained)?;
    Ok(true)
}

pub fn load_grants_from_path(path: &Path) -> Result<Vec<ApprovalGrant>> {
    if !path.exists() {
        return Ok(Vec::new());
    }

    let file = fs::File::open(path).context(format!("failed to open {}", path.display()))?;
    let reader = BufReader::new(file);
    let mut grants = Vec::new();

    for (index, line) in reader.lines().enumerate() {
        let line = line.context(format!("failed to read {}", path.display()))?;
        if line.trim().is_empty() {
            continue;
        }

        let grant = match serde_json::from_str::<ApprovalGrant>(&line) {
            Ok(grant) => grant,
            Err(error) => {
                anyhow::bail!(
                    "failed to parse grant on line {} of {}: {error}",
                    index + 1,
                    path.display()
                );
            }
        };
        grants.push(grant);
    }

    Ok(grants)
}

pub fn append_grant_to_path(path: &Path, grant: &ApprovalGrant) -> Result<()> {
    ensure_ward_home_for(path)?;
    let mut file = fs_util::open_private_append(path)?;
    let line = serde_json::to_string(grant).expect("approval grants should serialize");
    writeln!(file, "{line}").context(format!("failed to write {}", path.display()))
}

pub fn revoke_session_grants_at_path(path: &Path) -> Result<usize> {
    let grants = load_grants_from_path(path)?;
    if grants.is_empty() {
        return Ok(0);
    }

    let before = grants.len();
    let retained = grants
        .into_iter()
        .filter(|grant| grant.scope != ApprovalScope::Session)
        .collect::<Vec<_>>();
    let revoked = before - retained.len();

    write_grants_to_path(path, &retained)?;

    Ok(revoked)
}

pub fn revoke_grant_at_path(path: &Path, id: uuid::Uuid) -> Result<bool> {
    let grants = load_grants_from_path(path)?;
    let before = grants.len();
    let retained = grants
        .into_iter()
        .filter(|grant| grant.id != id)
        .collect::<Vec<_>>();
    if retained.len() == before {
        return Ok(false);
    }
    write_grants_to_path(path, &retained)?;
    Ok(true)
}

pub fn prune_expired_grants_at_path(path: &Path, now: DateTime<Utc>) -> Result<usize> {
    let grants = load_grants_from_path(path)?;
    let before = grants.len();
    let mut retained = Vec::new();
    for grant in grants {
        let retain = match grant.expires_at.as_ref() {
            Some(expires_at) => expires_at > &now,
            None => true,
        };
        if retain {
            retained.push(grant);
        }
    }
    let pruned = before - retained.len();
    if pruned > 0 {
        write_grants_to_path(path, &retained)?;
    }
    Ok(pruned)
}

fn write_grants_to_path(path: &Path, grants: &[ApprovalGrant]) -> Result<()> {
    ensure_ward_home_for(path)?;
    fs_util::ensure_private_parent_dir(path)?;
    let mut file = fs::File::create(path).context(format!("failed to write {}", path.display()))?;
    fs_util::set_private_file_permissions(path)?;
    for grant in grants {
        let line = serde_json::to_string(grant).expect("approval grants should serialize");
        writeln!(file, "{line}").context(format!("failed to write {}", path.display()))?;
    }
    Ok(())
}

fn ensure_ward_home_for(path: &Path) -> Result<()> {
    let home = logs::ward_home();
    if path.starts_with(&home) {
        fs_util::ensure_private_dir(&home)?;
    }
    Ok(())
}

pub fn find_matching_grant_in<'a>(
    grants: &'a [ApprovalGrant],
    access: &AccessRequest,
    now: DateTime<Utc>,
) -> Option<&'a ApprovalGrant> {
    grants
        .iter()
        .rev()
        .find(|grant| grant_matches_access(grant, access, now, false))
}

pub fn find_matching_grant_in_with_context<'a>(
    grants: &'a [ApprovalGrant],
    access: &AccessRequest,
    now: DateTime<Utc>,
    verified_context: &context::VerifiedContext,
) -> Option<&'a ApprovalGrant> {
    grants.iter().rev().find(|grant| {
        grant_matches_access_with_context(grant, access, now, false, Some(verified_context))
    })
}

pub fn find_matching_once_grant_in<'a>(
    grants: &'a [ApprovalGrant],
    access: &AccessRequest,
    now: DateTime<Utc>,
    critical_required: bool,
) -> Option<&'a ApprovalGrant> {
    grants.iter().rev().find(|grant| {
        grant.scope == ApprovalScope::Once
            && grant_matches_access(grant, access, now, critical_required)
    })
}

pub fn find_matching_once_grant_in_with_context<'a>(
    grants: &'a [ApprovalGrant],
    access: &AccessRequest,
    now: DateTime<Utc>,
    critical_required: bool,
    verified_context: &context::VerifiedContext,
) -> Option<&'a ApprovalGrant> {
    grants.iter().rev().find(|grant| {
        grant.scope == ApprovalScope::Once
            && grant_matches_access_with_context(
                grant,
                access,
                now,
                critical_required,
                Some(verified_context),
            )
    })
}

pub fn find_matching_non_always_grant_in<'a>(
    grants: &'a [ApprovalGrant],
    access: &AccessRequest,
    now: DateTime<Utc>,
) -> Option<&'a ApprovalGrant> {
    grants.iter().rev().find(|grant| {
        grant.scope != ApprovalScope::Always && grant_matches_access(grant, access, now, false)
    })
}

pub fn find_matching_non_always_grant_in_with_context<'a>(
    grants: &'a [ApprovalGrant],
    access: &AccessRequest,
    now: DateTime<Utc>,
    verified_context: &context::VerifiedContext,
) -> Option<&'a ApprovalGrant> {
    grants.iter().rev().find(|grant| {
        grant.scope != ApprovalScope::Always
            && grant_matches_access_with_context(grant, access, now, false, Some(verified_context))
    })
}

pub fn grant_integrity_status(grant: &ApprovalGrant, now: DateTime<Utc>) -> GrantIntegrityStatus {
    if grant
        .expires_at
        .as_ref()
        .is_some_and(|expires_at| expires_at <= &now)
    {
        return GrantIntegrityStatus::Expired;
    }
    if grant.receipt.is_none() {
        return GrantIntegrityStatus::LegacyUnsigned;
    }
    if receipt_matches_grant(grant) {
        GrantIntegrityStatus::Valid
    } else {
        GrantIntegrityStatus::Invalid
    }
}

fn grant_from_decision(
    access: &AccessRequest,
    decision: &ApprovalDecision,
    now: DateTime<Utc>,
) -> Result<ApprovalGrant> {
    if decision.scope == ApprovalScope::Branch && access.branch.is_none() {
        anyhow::bail!("branch-scoped approval requires a git branch");
    }

    let expires_at = match decision.scope {
        ApprovalScope::Session => Some(now + Duration::hours(SESSION_GRANT_HOURS)),
        ApprovalScope::Once => Some(now + Duration::minutes(15)),
        ApprovalScope::Branch | ApprovalScope::Always => None,
        ApprovalScope::Deny => anyhow::bail!(
            "{} cannot be persisted as an approval grant",
            decision.scope
        ),
    };

    Ok(ApprovalGrant {
        id: uuid::Uuid::new_v4(),
        created_at: now,
        expires_at,
        project: access.project.clone(),
        agent: access.agent.clone(),
        branch: access.branch.clone(),
        command: access.command.clone(),
        approved_env: decision.approved_env.clone(),
        scope: decision.scope,
        uses_remaining: (decision.scope == ApprovalScope::Once).then_some(1),
        receipt: None,
    })
}

fn grant_matches_access(
    grant: &ApprovalGrant,
    access: &AccessRequest,
    now: DateTime<Utc>,
    critical_required: bool,
) -> bool {
    grant_matches_access_with_context(grant, access, now, critical_required, None)
}

fn grant_matches_access_with_context(
    grant: &ApprovalGrant,
    access: &AccessRequest,
    now: DateTime<Utc>,
    critical_required: bool,
    verified_context: Option<&context::VerifiedContext>,
) -> bool {
    if grant.scope == ApprovalScope::Deny {
        return false;
    }
    if grant.scope == ApprovalScope::Once && grant.uses_remaining.unwrap_or(0) == 0 {
        return false;
    }
    if grant
        .expires_at
        .as_ref()
        .is_some_and(|expires_at| expires_at <= &now)
    {
        return false;
    }
    if grant.project != access.project || grant.command != access.command {
        return false;
    }
    let Some(receipt) = grant.receipt.as_ref() else {
        return false;
    };
    if !receipt_matches_grant(grant) {
        return false;
    }
    if !receipt_matches_verified_context(receipt, verified_context) {
        return false;
    }
    if critical_required && !receipt.payload.critical_confirmation {
        return false;
    }
    let agent_mismatch = grant
        .agent
        .as_deref()
        .zip(access.agent.as_deref())
        .is_some_and(|(grant_agent, request_agent)| grant_agent != request_agent);
    if agent_mismatch {
        return false;
    }

    if grant.scope == ApprovalScope::Branch && grant.branch.as_deref() != access.branch.as_deref() {
        return false;
    }

    let approved = receipt
        .payload
        .approved_env
        .iter()
        .map(String::as_str)
        .collect::<BTreeSet<_>>();
    access
        .env
        .iter()
        .all(|env_name| approved.contains(env_name.as_str()))
}

fn receipt_matches_verified_context(
    receipt: &ApprovalReceipt,
    verified_context: Option<&context::VerifiedContext>,
) -> bool {
    let payload = &receipt.payload;
    let has_bound_context = payload.agent_key_id.is_some()
        || payload.verified_worktree.is_some()
        || payload.verified_git_remote.is_some()
        || payload.verified_commit.is_some();
    match (verified_context, has_bound_context) {
        (Some(verified), _) => {
            payload.agent_key_id.as_deref() == Some(verified.agent_key_id.as_str())
                && payload.verified_worktree.as_ref() == Some(&verified.worktree)
                && payload.verified_git_remote.as_deref() == Some(verified.git_remote.as_str())
                && payload.verified_commit.as_deref() == Some(verified.commit.as_str())
        }
        (None, true) => false,
        (None, false) => true,
    }
}

fn sign_grant(
    access: &AccessRequest,
    vault: &Path,
    grant: &mut ApprovalGrant,
    context: GrantReceiptContext,
) -> Result<()> {
    #[cfg(not(any(test, coverage)))]
    {
        let broker_payload = approval_receipts::build_payload_with_context(
            access,
            grant.id,
            context.request_id,
            &grant.approved_env,
            grant.scope,
            grant.expires_at,
            context.critical_confirmation,
            grant.created_at,
            String::new(),
            context.verified_context.as_ref(),
        );
        let receipt = broker::sign_receipt(&access.project, vault, broker_payload)
            .map_err(|error| anyhow::anyhow!("signing_key_unavailable: {error}"))?;
        grant.receipt = Some(receipt);
        return Ok(());
    }

    #[cfg(any(test, coverage))]
    {
        let signing_key = match unlock::active_run_signing_key(&access.project, vault)? {
            unlock::RunSigningLookup::Available(signing_key) => signing_key,
            unlock::RunSigningLookup::Missing => anyhow::bail!(
                "signing_key_unavailable: run ward unlock --ttl 8h before creating approval grants"
            ),
            unlock::RunSigningLookup::MaterialUnavailable { reason } => {
                anyhow::bail!("{reason}")
            }
        };
        let payload = approval_receipts::build_payload_with_context(
            access,
            grant.id,
            context.request_id,
            &grant.approved_env,
            grant.scope,
            grant.expires_at,
            context.critical_confirmation,
            grant.created_at,
            signing_key.signer_key_id.clone(),
            context.verified_context.as_ref(),
        );
        let receipt = approval_receipts::sign_payload(payload, &signing_key)
            .expect("grant payload signer id is built from the active signing key");
        grant.receipt = Some(receipt);
        Ok(())
    }
}

fn receipt_matches_grant(grant: &ApprovalGrant) -> bool {
    let Some(receipt) = grant.receipt.as_ref() else {
        return false;
    };
    let payload = &receipt.payload;
    payload.schema_version == 1
        && payload.grant_id == grant.id
        && payload.project == grant.project
        && payload.agent == grant.agent
        && payload.branch == grant.branch
        && payload.command_hash == approval_receipts::command_hash(&grant.command)
        && payload.approved_env == sorted_strings(&grant.approved_env)
        && payload.scope == grant.scope
        && payload.expires_at == grant.expires_at
        && payload.created_at == grant.created_at
        && payload.signer_key_id == receipt.signer_key_id
        && payload
            .agent_key_id
            .as_ref()
            .map_or(true, |value| !value.is_empty())
        && approval_receipts::verify_receipt_signature(&grant.project, receipt)
}

fn sorted_strings(values: &[String]) -> Vec<String> {
    let mut sorted = values.to_vec();
    sorted.sort();
    sorted
}

impl ApprovalScope {
    pub fn is_persisted_grant(self) -> bool {
        matches!(
            self,
            ApprovalScope::Session | ApprovalScope::Branch | ApprovalScope::Always
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        approvals::{ApprovalDecision, ApprovalSource},
        unlock,
    };
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
    }

    fn access() -> AccessRequest {
        AccessRequest {
            project: "ambienta".to_string(),
            agent: Some("codex".to_string()),
            branch: Some("feature/x".to_string()),
            action: Some("Run dev server".to_string()),
            command: "pnpm dev".to_string(),
            env: vec!["DATABASE_URL".to_string()],
        }
    }

    fn setup_signing_home() -> (tempfile::TempDir, PathBuf) {
        let tempdir = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", tempdir.path());
        std::env::set_var("WARD_UNSAFE_TEST_KEYRING", "1");
        let vault = tempdir.path().join(".env.vault");
        unlock::create_run_unlock("ambienta", &vault, "1234", Duration::hours(1)).unwrap();
        (tempdir, vault)
    }

    fn clear_signing_home() {
        std::env::remove_var("WARD_HOME");
        std::env::remove_var("WARD_UNSAFE_TEST_KEYRING");
    }

    fn grant(scope: ApprovalScope, now: DateTime<Utc>, vault: &Path) -> ApprovalGrant {
        let mut grant = ApprovalGrant {
            id: uuid::Uuid::new_v4(),
            created_at: now,
            expires_at: None,
            project: "ambienta".to_string(),
            agent: Some("codex".to_string()),
            branch: Some("feature/x".to_string()),
            command: "pnpm dev".to_string(),
            approved_env: vec!["DATABASE_URL".to_string(), "PAYLOAD_SECRET".to_string()],
            scope,
            uses_remaining: (scope == ApprovalScope::Once).then_some(1),
            receipt: None,
        };
        let access = access();
        sign_grant(
            &access,
            vault,
            &mut grant,
            GrantReceiptContext::synthetic(false),
        )
        .unwrap();
        grant
    }

    fn verified_context() -> context::VerifiedContext {
        context::VerifiedContext {
            project: "ambienta".to_string(),
            agent: "codex".to_string(),
            agent_key_id: "agent:key".to_string(),
            worktree: PathBuf::from("/tmp/ambienta-worktree"),
            branch: "feature/x".to_string(),
            git_remote: "https://example.test/ambienta".to_string(),
            commit: "abc123".to_string(),
            git_common_dir: None,
        }
    }

    #[test]
    #[serial_test::serial]
    fn matches_session_branch_and_always_grants() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let now = Utc::now();
        let access = access();

        for scope in [
            ApprovalScope::Session,
            ApprovalScope::Branch,
            ApprovalScope::Always,
        ] {
            let grant = grant(scope, now, &vault);
            assert!(grant_matches_access(&grant, &access, now, false));
        }
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn ignores_once_deny_and_expired_session_grants() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let now = Utc::now();
        let access = access();
        let mut expired = grant(ApprovalScope::Session, now, &vault);
        expired.expires_at = Some(now - Duration::minutes(1));
        let mut spent_once = grant(ApprovalScope::Once, now, &vault);
        spent_once.uses_remaining = Some(0);

        assert!(grant_matches_access(
            &grant(ApprovalScope::Once, now, &vault),
            &access,
            now,
            false
        ));
        assert!(!grant_matches_access(
            &grant(ApprovalScope::Deny, now, &vault),
            &access,
            now,
            false
        ));
        assert!(!grant_matches_access(&spent_once, &access, now, false));
        assert!(!grant_matches_access(&expired, &access, now, false));
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn requires_requested_env_subset() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let now = Utc::now();
        let mut access = access();
        access.env.push("OPENAI_API_KEY".to_string());

        assert!(!grant_matches_access(
            &grant(ApprovalScope::Always, now, &vault),
            &access,
            now,
            false
        ));
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn finds_latest_matching_grant_or_once_only_grant() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let now = Utc::now();
        let access = access();
        let always = grant(ApprovalScope::Always, now, &vault);
        let once = grant(ApprovalScope::Once, now, &vault);
        let grants = vec![once.clone(), always.clone()];

        assert_eq!(
            find_matching_grant_in(&grants, &access, now).unwrap().id,
            always.id
        );
        assert_eq!(
            find_matching_once_grant_in(&grants, &access, now, false)
                .unwrap()
                .id,
            once.id
        );
        assert_eq!(
            find_matching_non_always_grant_in(&grants, &access, now)
                .unwrap()
                .id,
            once.id
        );
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn lock_revokes_only_session_grants() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let tempdir = tempfile::tempdir().unwrap();
        let path = tempdir.path().join("grants.jsonl");
        let now = Utc::now();

        append_grant_to_path(&path, &grant(ApprovalScope::Session, now, &vault)).unwrap();
        append_grant_to_path(&path, &grant(ApprovalScope::Branch, now, &vault)).unwrap();
        append_grant_to_path(&path, &grant(ApprovalScope::Always, now, &vault)).unwrap();

        let revoked = revoke_session_grants_at_path(&path).unwrap();
        let retained = load_grants_from_path(&path).unwrap();

        assert_eq!(revoked, 1);
        assert_eq!(retained.len(), 2);
        assert!(retained
            .iter()
            .all(|grant| grant.scope != ApprovalScope::Session));
        clear_signing_home();
    }

    #[test]
    fn load_grants_skips_blank_lines_and_reports_invalid_json() {
        let tempdir = tempfile::tempdir().unwrap();
        let blank_path = tempdir.path().join("blank.jsonl");
        let bad_path = tempdir.path().join("bad.jsonl");

        std::fs::write(&blank_path, "\n\n").unwrap();
        std::fs::write(&bad_path, "{not-json}\n").unwrap();

        assert!(load_grants_from_path(&tempdir.path().join("missing.jsonl"))
            .unwrap()
            .is_empty());
        assert!(load_grants_from_path(&blank_path).unwrap().is_empty());
        assert!(load_grants_from_path(&bad_path).is_err());
    }

    #[test]
    #[serial_test::serial]
    fn persist_grant_writes_only_prompt_persisted_approvals() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let access = access();
        let decision = ApprovalDecision {
            approved: true,
            scope: ApprovalScope::Always,
            approved_env: vec!["DATABASE_URL".to_string()],
            denied_env: Vec::new(),
            source: ApprovalSource::LocalTty,
            grant_id: None,
        };

        let grant = persist_grant(
            &access,
            &decision,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap()
        .unwrap();
        let found = find_matching_grant(&access).unwrap().unwrap();
        let default_context_grant = persist_grant(&access, &decision, &vault, None)
            .unwrap()
            .unwrap();
        assert!(default_context_grant.receipt.is_some());

        clear_signing_home();
        assert_eq!(grant.id, found.id);
    }

    #[test]
    #[serial_test::serial]
    fn persist_grant_reports_append_failures() {
        let _guard = env_lock();
        let tempdir = tempfile::tempdir().unwrap();
        let sessions_path = tempdir.path().join("sessions");
        std::fs::write(&sessions_path, "").unwrap();
        std::env::set_var("WARD_HOME", tempdir.path());
        std::env::set_var("WARD_UNSAFE_TEST_KEYRING", "1");
        let vault = tempdir.path().join(".env.vault");
        assert!(unlock::create_run_unlock("ambienta", &vault, "1234", Duration::hours(1)).is_err());

        let decision = ApprovalDecision {
            approved: true,
            scope: ApprovalScope::Always,
            approved_env: vec!["DATABASE_URL".to_string()],
            denied_env: Vec::new(),
            source: ApprovalSource::LocalTty,
            grant_id: None,
        };

        assert!(persist_grant(
            &access(),
            &decision,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .is_err());
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn persist_grant_writes_session_scope() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();

        let decision = ApprovalDecision {
            approved: true,
            scope: ApprovalScope::Session,
            approved_env: vec!["DATABASE_URL".to_string()],
            denied_env: Vec::new(),
            source: ApprovalSource::LocalTty,
            grant_id: None,
        };
        let grant = persist_grant(
            &access(),
            &decision,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap()
        .unwrap();

        assert_eq!(grant.scope, ApprovalScope::Session);
        assert!(grant.expires_at.is_some());
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn context_wrapper_lookups_and_default_manual_receipts_are_exercised() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let access = access();
        let context = verified_context();

        let default_grant = persist_manual_grant(
            &access,
            ApprovalScope::Session,
            ApprovalSource::ManualAllow,
            &vault,
            None,
        )
        .unwrap();
        assert!(default_grant.receipt.is_some());

        let receipt_context = GrantReceiptContext {
            request_id: uuid::Uuid::new_v4(),
            critical_confirmation: false,
            verified_context: Some(context.clone()),
        };
        let once_grant = persist_manual_grant(
            &access,
            ApprovalScope::Once,
            ApprovalSource::ManualAllow,
            &vault,
            Some(receipt_context),
        )
        .unwrap();
        assert!(once_grant.receipt.is_some());
        assert!(
            find_matching_non_always_grant_with_context(&access, &context)
                .unwrap()
                .is_some()
        );
        assert!(
            find_matching_once_grant_with_context(&access, false, &context)
                .unwrap()
                .is_some()
        );
        assert_eq!(
            prune_expired_grants_at_path(&grants_path(), Utc::now()).unwrap(),
            0
        );

        clear_signing_home();
    }

    #[test]
    fn append_grant_reports_open_failures() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let tempdir = tempfile::tempdir().unwrap();
        let directory = tempdir.path().join("grants.jsonl");
        std::fs::create_dir(&directory).unwrap();

        assert!(append_grant_to_path(
            &directory,
            &grant(ApprovalScope::Always, Utc::now(), &vault)
        )
        .is_err());
        clear_signing_home();
    }

    #[test]
    fn persist_grant_ignores_once_deny_policy_and_rejects_missing_branch_scope() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let access = AccessRequest {
            branch: None,
            ..access()
        };
        let mut decision = ApprovalDecision {
            approved: true,
            scope: ApprovalScope::Once,
            approved_env: vec!["DATABASE_URL".to_string()],
            denied_env: Vec::new(),
            source: ApprovalSource::LocalTty,
            grant_id: None,
        };

        assert!(persist_grant(
            &access,
            &decision,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap()
        .is_none());
        decision.scope = ApprovalScope::Deny;
        decision.approved = false;
        assert!(persist_grant(
            &access,
            &decision,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap()
        .is_none());
        decision.scope = ApprovalScope::Always;
        decision.approved = true;
        decision.source = ApprovalSource::PolicyAuto;
        assert!(persist_grant(
            &access,
            &decision,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap()
        .is_none());
        decision.scope = ApprovalScope::Branch;
        decision.source = ApprovalSource::LocalTty;
        assert!(persist_grant(
            &access,
            &decision,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .is_err());
        clear_signing_home();
    }

    #[test]
    fn grant_from_decision_creates_once_grants_and_rejects_denials() {
        let access = access();
        let mut decision = ApprovalDecision {
            approved: true,
            scope: ApprovalScope::Once,
            approved_env: vec!["DATABASE_URL".to_string()],
            denied_env: Vec::new(),
            source: ApprovalSource::LocalTty,
            grant_id: None,
        };

        let once = grant_from_decision(&access, &decision, Utc::now()).unwrap();
        assert_eq!(once.scope, ApprovalScope::Once);
        assert_eq!(once.uses_remaining, Some(1));
        assert!(once.expires_at.is_some());
        decision.scope = ApprovalScope::Deny;
        assert!(grant_from_decision(&access, &decision, Utc::now()).is_err());
    }

    #[test]
    #[serial_test::serial]
    fn unsigned_and_modified_grants_are_not_reused() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let now = Utc::now();
        let access = access();
        let unsigned = grant_from_decision(
            &access,
            &ApprovalDecision {
                approved: true,
                scope: ApprovalScope::Always,
                approved_env: access.env.clone(),
                denied_env: Vec::new(),
                source: ApprovalSource::LocalTty,
                grant_id: None,
            },
            now,
        )
        .unwrap();
        assert_eq!(
            grant_integrity_status(&unsigned, now),
            GrantIntegrityStatus::LegacyUnsigned
        );
        assert!(!receipt_matches_grant(&unsigned));
        assert!(!grant_matches_access(&unsigned, &access, now, false));

        let mut signed = grant(ApprovalScope::Always, now, &vault);
        assert_eq!(
            grant_integrity_status(&signed, now),
            GrantIntegrityStatus::Valid
        );
        let mut expired = signed.clone();
        expired.expires_at = Some(now - Duration::minutes(1));
        assert_eq!(
            grant_integrity_status(&expired, now),
            GrantIntegrityStatus::Expired
        );
        assert!(!grant_matches_access(&signed, &access, now, true));

        let mut broken_receipt = signed.clone();
        broken_receipt.receipt.as_mut().unwrap().payload_hash = "bad".to_string();
        assert!(!grant_matches_access(&broken_receipt, &access, now, false));

        signed.command = "pnpm build".to_string();
        assert_eq!(
            grant_integrity_status(&signed, now),
            GrantIntegrityStatus::Invalid
        );
        assert!(!grant_matches_access(&signed, &access, now, false));
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn persist_manual_grant_reports_unavailable_signing_material() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let key_store_path = crate::logs::ward_home().join("cache").join("keystore.json");
        std::fs::remove_file(key_store_path).unwrap();

        let error = persist_manual_grant(
            &access(),
            ApprovalScope::Always,
            ApprovalSource::ManualAllow,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap_err()
        .to_string();

        assert!(error.contains("unlock_material_unavailable"));
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn grant_matching_rejects_project_command_agent_and_branch_mismatches() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let now = Utc::now();
        let grant = grant(ApprovalScope::Branch, now, &vault);

        let mut project_mismatch = access();
        project_mismatch.project = "other".to_string();
        assert!(!grant_matches_access(&grant, &project_mismatch, now, false));

        let mut command_mismatch = access();
        command_mismatch.command = "pnpm build".to_string();
        assert!(!grant_matches_access(&grant, &command_mismatch, now, false));

        let mut agent_mismatch = access();
        agent_mismatch.agent = Some("cursor".to_string());
        assert!(!grant_matches_access(&grant, &agent_mismatch, now, false));

        let mut branch_mismatch = access();
        branch_mismatch.branch = Some("feature/other".to_string());
        assert!(!grant_matches_access(&grant, &branch_mismatch, now, false));
        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn context_bound_grants_require_matching_verified_context() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();
        let now = Utc::now();
        let access = access();
        let verified = verified_context();
        let mut grant = ApprovalGrant {
            id: uuid::Uuid::new_v4(),
            created_at: now,
            expires_at: Some(now + Duration::hours(1)),
            project: access.project.clone(),
            agent: access.agent.clone(),
            branch: access.branch.clone(),
            command: access.command.clone(),
            approved_env: access.env.clone(),
            scope: ApprovalScope::Session,
            uses_remaining: None,
            receipt: None,
        };
        sign_grant(
            &access,
            &vault,
            &mut grant,
            GrantReceiptContext {
                request_id: uuid::Uuid::new_v4(),
                critical_confirmation: false,
                verified_context: Some(verified.clone()),
            },
        )
        .unwrap();
        assert!(find_matching_non_always_grant_in_with_context(
            std::slice::from_ref(&grant),
            &access,
            now,
            &verified
        )
        .is_some());
        assert!(!grant_matches_access(&grant, &access, now, false));
        clear_signing_home();
    }

    #[test]
    fn revoke_session_grants_handles_missing_file() {
        let tempdir = tempfile::tempdir().unwrap();
        assert_eq!(
            revoke_session_grants_at_path(&tempdir.path().join("missing.jsonl")).unwrap(),
            0
        );
    }

    #[test]
    #[serial_test::serial]
    fn manual_revoke_prune_and_once_consumption_helpers_cover_edges() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();

        let access = access();
        assert!(persist_manual_grant(
            &access,
            ApprovalScope::Always,
            ApprovalSource::Grant,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .is_err());
        assert!(persist_manual_grant(
            &access,
            ApprovalScope::Deny,
            ApprovalSource::ManualAllow,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .is_err());

        let always = persist_manual_grant(
            &access,
            ApprovalScope::Always,
            ApprovalSource::ManualAllow,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap();
        let once = persist_manual_grant(
            &access,
            ApprovalScope::Once,
            ApprovalSource::AgentMediated,
            &vault,
            Some(GrantReceiptContext::synthetic(false)),
        )
        .unwrap();
        let session = persist_manual_grant(
            &access,
            ApprovalScope::Session,
            ApprovalSource::ManualAllow,
            &vault,
            None,
        )
        .unwrap();
        assert_eq!(session.scope, ApprovalScope::Session);

        assert!(!consume_once_grant(uuid::Uuid::new_v4()).unwrap());
        assert!(consume_once_grant(once.id).unwrap());
        assert!(!revoke_grant(uuid::Uuid::new_v4()).unwrap());
        assert!(revoke_grant(always.id).unwrap());

        let mut expired = grant(ApprovalScope::Session, Utc::now(), &vault);
        expired.expires_at = Some(Utc::now() - Duration::minutes(1));
        append_grant_to_path(&grants_path(), &expired).unwrap();
        assert_eq!(prune_expired_grants().unwrap(), 1);
        assert_eq!(prune_expired_grants().unwrap(), 0);

        clear_signing_home();
    }

    #[test]
    #[serial_test::serial]
    fn global_grant_helpers_report_invalid_grant_file() {
        let _guard = env_lock();
        let tempdir = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", tempdir.path());
        let grants_dir = tempdir.path().join("sessions");
        std::fs::create_dir_all(&grants_dir).unwrap();
        std::fs::write(grants_dir.join("grants.jsonl"), "{bad-json}\n").unwrap();

        assert!(find_matching_grant(&access()).is_err());
        assert!(find_matching_once_grant(&access(), false).is_err());
        assert!(find_matching_non_always_grant(&access()).is_err());
        assert!(revoke_session_grants().is_err());

        std::env::remove_var("WARD_HOME");
    }

    #[test]
    #[serial_test::serial]
    fn removes_project_grants_and_keeps_other_projects() {
        let _guard = env_lock();
        let (_home, vault) = setup_signing_home();

        let now = Utc::now();
        append_grant_to_path(&grants_path(), &grant(ApprovalScope::Always, now, &vault)).unwrap();
        let mut other = grant(ApprovalScope::Always, now, &vault);
        other.project = "other".to_string();
        append_grant_to_path(&grants_path(), &other).unwrap();

        assert_eq!(remove_project_grants("missing").unwrap(), 0);
        assert_eq!(remove_project_grants("ambienta").unwrap(), 1);
        let retained = load_grants().unwrap();
        assert_eq!(retained.len(), 1);
        assert_eq!(retained[0].project, "other");

        clear_signing_home();
    }
}
