use std::cell::Cell;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Barrier;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::{Duration, Instant};

use rebecca_core::config::AppPaths;
use rebecca_core::executor::{
    CleanupBackend, ExecutionOutcome, execute_cleanup_plan,
    execute_cleanup_plan_parallel_with_policy, execute_cleanup_plan_with_policy,
};
use rebecca_core::plan::{CleanupPlan, CleanupTarget, CleanupTargetIssueReason};
use rebecca_core::protection::ProtectionPolicy;
use rebecca_core::{CleanupWorkflow, DeleteMode, PlanRequest, Platform, Result, TargetStatus};

#[test]
fn executor_marks_allowed_targets_completed_and_keeps_blocked_targets() {
    let temp = tempfile::tempdir().unwrap();
    let file = temp.path().join("file.tmp");
    fs::write(&file, b"trash").unwrap();
    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.user-temp",
        file,
        10,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::blocked_with_reason_code(
        "windows.user-temp",
        PathBuf::from("C:/Windows"),
        DeleteMode::RecycleBin,
        CleanupTargetIssueReason::SafetyPolicyBlocked,
        "protected",
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    execute_cleanup_plan(&mut plan, &backend).unwrap();

    assert_eq!(backend.calls.get(), 1);
    assert_eq!(plan.targets[0].status, TargetStatus::Completed);
    assert_eq!(plan.targets[0].pending_reclaim_bytes, 10);
    assert_eq!(plan.targets[1].status, TargetStatus::Blocked);
}

#[test]
fn executor_records_failure_without_aborting_plan() {
    let temp = tempfile::tempdir().unwrap();
    let file = temp.path().join("file.tmp");
    fs::write(&file, b"trash").unwrap();
    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.user-temp",
        file,
        10,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::failure();
    execute_cleanup_plan(&mut plan, &backend).unwrap();

    assert_eq!(plan.targets[0].status, TargetStatus::Failed);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::ExecutionFailed)
    );
    assert_eq!(plan.summary.failed_targets, 1);
    assert_eq!(
        plan.summary.issue_matrix[0].reason_code,
        CleanupTargetIssueReason::ExecutionFailed
    );
}

#[test]
fn executor_revalidates_protected_category_targets_before_backend_calls() {
    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.custom-browser-history",
        PathBuf::from("C:/Users/Alice/AppData/Local/Google/Chrome/User Data/Default/History"),
        10,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    execute_cleanup_plan_with_policy(&mut plan, &backend, ProtectionPolicy::new()).unwrap();

    assert_eq!(backend.calls.get(), 0);
    assert_eq!(plan.targets[0].status, TargetStatus::Blocked);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::SafetyPolicyBlocked)
    );
    assert!(
        plan.targets[0]
            .reason
            .as_deref()
            .unwrap()
            .contains("browser private data")
    );
    assert_eq!(plan.summary.blocked_targets, 1);
}

#[test]
fn executor_revalidates_rebecca_owned_storage_before_backend_calls() {
    let app_paths = AppPaths {
        config_dir: PathBuf::from("C:/Users/Alice/AppData/Roaming/Rebecca"),
        config_file: PathBuf::from("C:/Users/Alice/AppData/Roaming/Rebecca/config.toml"),
        state_dir: PathBuf::from("C:/Users/Alice/AppData/Local/Rebecca/state"),
        cache_dir: PathBuf::from("C:/Users/Alice/AppData/Local/Rebecca/cache"),
        history_file: PathBuf::from("C:/Users/Alice/AppData/Local/Rebecca/state/history.jsonl"),
    };
    let protected_storage = app_paths.storage_entries();
    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.custom-rebecca-cache",
        app_paths.cache_dir.join("scan"),
        10,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    let policy = ProtectionPolicy::new().with_protected_storage(&protected_storage);
    execute_cleanup_plan_with_policy(&mut plan, &backend, policy).unwrap();

    assert_eq!(backend.calls.get(), 0);
    assert_eq!(plan.targets[0].status, TargetStatus::Blocked);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::SafetyPolicyBlocked)
    );
    assert!(
        plan.targets[0]
            .reason
            .as_deref()
            .unwrap()
            .contains("Rebecca-owned Cache dir")
    );
    assert_eq!(plan.summary.blocked_targets, 1);
}

#[test]
fn executor_revalidates_user_protected_paths_before_backend_calls() {
    let temp = tempfile::tempdir().unwrap();
    let cache_dir = temp.path().join("Slack").join("Cache");
    fs::create_dir_all(&cache_dir).unwrap();
    fs::write(cache_dir.join("cache.bin"), b"trash").unwrap();
    let protected_paths = vec![cache_dir.clone()];
    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.slack-cache",
        cache_dir,
        5,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    let policy = ProtectionPolicy::new().with_protected_paths(&protected_paths);
    execute_cleanup_plan_with_policy(&mut plan, &backend, policy).unwrap();

    assert_eq!(backend.calls.get(), 0);
    assert_eq!(plan.targets[0].status, TargetStatus::Blocked);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::SafetyPolicyBlocked)
    );
    assert!(
        plan.targets[0]
            .reason
            .as_deref()
            .unwrap()
            .contains("user-protected path")
    );
    assert_eq!(plan.summary.blocked_targets, 1);
}

#[test]
fn executor_revalidates_project_artifact_targets_before_backend_calls() {
    let temp = tempfile::tempdir().unwrap();
    let target_dir = temp.path().join("project").join("target");
    fs::create_dir_all(&target_dir).unwrap();
    fs::write(target_dir.join("cache.bin"), b"trash").unwrap();
    let protected_paths = vec![target_dir.clone()];
    let mut request = PlanRequest::for_platform(Platform::Windows, DeleteMode::RecycleBin);
    request.workflow = CleanupWorkflow::ProjectArtifacts;
    let mut plan = CleanupPlan::empty(request);
    plan.targets.push(CleanupTarget::allowed(
        "windows.project-artifact-target",
        target_dir,
        5,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    let policy = ProtectionPolicy::new().with_protected_paths(&protected_paths);
    execute_cleanup_plan_with_policy(&mut plan, &backend, policy).unwrap();

    assert_eq!(backend.calls.get(), 0);
    assert_eq!(plan.targets[0].status, TargetStatus::Blocked);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::SafetyPolicyBlocked)
    );
}

#[test]
fn executor_skips_missing_targets_before_backend_calls() {
    let temp = tempfile::tempdir().unwrap();
    let missing_file = temp.path().join("definitely-missing.tmp");
    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.user-temp",
        missing_file,
        10,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    execute_cleanup_plan_with_policy(&mut plan, &backend, ProtectionPolicy::new()).unwrap();

    assert_eq!(backend.calls.get(), 0);
    assert_eq!(plan.targets[0].status, TargetStatus::Skipped);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::ExecutionTargetMissing)
    );
    assert!(
        plan.targets[0]
            .reason
            .as_deref()
            .unwrap()
            .contains("path does not exist")
    );
    assert_eq!(plan.summary.skipped_targets, 1);
    assert_eq!(
        plan.summary.issue_matrix[0].reason_code,
        CleanupTargetIssueReason::ExecutionTargetMissing
    );
}

#[test]
fn executor_allows_app_leftover_cache_targets_after_revalidation() {
    let temp = tempfile::tempdir().unwrap();
    let cache_dir = temp
        .path()
        .join("AppData")
        .join("Local")
        .join("Example App")
        .join("Cache");
    fs::create_dir_all(&cache_dir).unwrap();
    fs::write(cache_dir.join("cache.bin"), b"trash").unwrap();
    let mut plan = CleanupPlan::empty(
        PlanRequest::for_platform(Platform::Windows, DeleteMode::RecycleBin)
            .with_workflow(CleanupWorkflow::AppLeftovers),
    );
    plan.targets.push(CleanupTarget::allowed(
        "windows.app-leftover-local-cache",
        cache_dir,
        5,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    execute_cleanup_plan_with_policy(&mut plan, &backend, ProtectionPolicy::new()).unwrap();

    assert_eq!(backend.calls.get(), 1);
    assert_eq!(plan.targets[0].status, TargetStatus::Completed);
    assert_eq!(plan.targets[0].pending_reclaim_bytes, 5);
}

#[test]
fn executor_skips_missing_app_leftover_targets_before_backend_calls() {
    let temp = tempfile::tempdir().unwrap();
    let missing_cache_dir = temp
        .path()
        .join("AppData")
        .join("Local")
        .join("Example App")
        .join("Cache");
    let mut plan = CleanupPlan::empty(
        PlanRequest::for_platform(Platform::Windows, DeleteMode::RecycleBin)
            .with_workflow(CleanupWorkflow::AppLeftovers),
    );
    plan.targets.push(CleanupTarget::allowed(
        "windows.app-leftover-local-cache",
        missing_cache_dir,
        5,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    execute_cleanup_plan_with_policy(&mut plan, &backend, ProtectionPolicy::new()).unwrap();

    assert_eq!(backend.calls.get(), 0);
    assert_eq!(plan.targets[0].status, TargetStatus::Skipped);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::ExecutionTargetMissing)
    );
    assert_eq!(
        plan.targets[0].reason.as_deref(),
        Some("path does not exist")
    );
    assert_eq!(plan.summary.skipped_targets, 1);
}

#[test]
fn executor_parallel_batches_independent_targets() {
    let temp = tempfile::tempdir().unwrap();
    let first = temp.path().join("first.tmp");
    let second = temp.path().join("second.tmp");
    fs::write(&first, b"trash").unwrap();
    fs::write(&second, b"trash").unwrap();
    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.first",
        first,
        10,
        DeleteMode::RecycleBin,
    ));
    plan.targets.push(CleanupTarget::allowed(
        "windows.second",
        second,
        20,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = Arc::new(BlockingBackend::new());
    let backend_for_thread = Arc::clone(&backend);
    let delete_thread = thread::spawn(move || {
        execute_cleanup_plan_parallel_with_policy(
            &mut plan,
            backend_for_thread.as_ref(),
            ProtectionPolicy::new(),
        )
        .unwrap();
        plan
    });

    let deadline = Instant::now() + Duration::from_secs(1);
    while backend.started.load(Ordering::SeqCst) < 2 && Instant::now() < deadline {
        thread::sleep(Duration::from_millis(10));
    }

    assert_eq!(
        backend.started.load(Ordering::SeqCst),
        2,
        "expected both deletes to be in flight at once"
    );
    backend.gate.wait();

    let plan = delete_thread.join().unwrap();
    assert_eq!(backend.max_active.load(Ordering::SeqCst), 2);
    assert_eq!(plan.summary.completed_targets, 2);
    assert_eq!(plan.targets[0].status, TargetStatus::Completed);
    assert_eq!(plan.targets[1].status, TargetStatus::Completed);
}

#[test]
fn executor_blocks_app_leftover_durable_paths_before_backend_calls() {
    let mut plan = CleanupPlan::empty(
        PlanRequest::for_platform(Platform::Windows, DeleteMode::RecycleBin)
            .with_workflow(CleanupWorkflow::AppLeftovers),
    );
    plan.targets.push(CleanupTarget::allowed(
        "windows.app-leftover-local-cache",
        PathBuf::from("C:/Users/Alice/AppData/Local/Example App/Local Storage"),
        10,
        DeleteMode::RecycleBin,
    ));
    plan.recompute_summary();

    let backend = FakeBackend::success();
    execute_cleanup_plan_with_policy(&mut plan, &backend, ProtectionPolicy::new()).unwrap();

    assert_eq!(backend.calls.get(), 0);
    assert_eq!(plan.targets[0].status, TargetStatus::Blocked);
    assert_eq!(
        plan.targets[0].reason_code,
        Some(CleanupTargetIssueReason::SafetyPolicyBlocked)
    );
    assert!(
        plan.targets[0]
            .reason
            .as_deref()
            .unwrap()
            .contains("application durable data")
    );
    assert_eq!(plan.summary.blocked_targets, 1);
}

struct FakeBackend {
    calls: Cell<usize>,
    fail: bool,
}

impl FakeBackend {
    fn success() -> Self {
        Self {
            calls: Cell::new(0),
            fail: false,
        }
    }

    fn failure() -> Self {
        Self {
            calls: Cell::new(0),
            fail: true,
        }
    }
}

impl CleanupBackend for FakeBackend {
    fn delete(&self, target: &CleanupTarget) -> Result<ExecutionOutcome> {
        self.calls.set(self.calls.get() + 1);
        if self.fail {
            return Err(rebecca_core::RebeccaError::ExecutionFailed(
                "permission denied".to_string(),
            ));
        }

        Ok(ExecutionOutcome {
            freed_bytes: 0,
            pending_reclaim_bytes: target.estimated_bytes,
            note: Some("fake delete".to_string()),
        })
    }
}

struct BlockingBackend {
    started: AtomicUsize,
    max_active: AtomicUsize,
    gate: Arc<Barrier>,
}

impl BlockingBackend {
    fn new() -> Self {
        Self {
            started: AtomicUsize::new(0),
            max_active: AtomicUsize::new(0),
            gate: Arc::new(Barrier::new(3)),
        }
    }
}

impl CleanupBackend for BlockingBackend {
    fn delete(&self, target: &CleanupTarget) -> Result<ExecutionOutcome> {
        let active = self.started.fetch_add(1, Ordering::SeqCst) + 1;
        loop {
            let current = self.max_active.load(Ordering::SeqCst);
            if active <= current {
                break;
            }
            if self
                .max_active
                .compare_exchange(current, active, Ordering::SeqCst, Ordering::SeqCst)
                .is_ok()
            {
                break;
            }
        }

        self.gate.wait();
        self.started.fetch_sub(1, Ordering::SeqCst);

        Ok(ExecutionOutcome {
            freed_bytes: 0,
            pending_reclaim_bytes: target.estimated_bytes,
            note: Some("blocking delete".to_string()),
        })
    }
}
