use std::fs;
use std::path::{Path, PathBuf};

mod common;

fn write_fixture_file(path: impl AsRef<Path>, bytes: &[u8]) {
    let path = path.as_ref();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    fs::write(path, bytes).unwrap();
}

fn appdata_roots(temp: &tempfile::TempDir) -> (PathBuf, PathBuf) {
    (
        temp.path().join("AppData").join("Local"),
        temp.path().join("AppData").join("Roaming"),
    )
}

fn write_app_leftover_fixture(temp: &tempfile::TempDir, app_name: &str) {
    let app = temp.path().join("AppData").join("Local").join(app_name);
    write_fixture_file(app.join("Cache").join("cache.bin"), b"abc");
    write_fixture_file(app.join("Local Storage").join("state.bin"), b"keep");
}

#[test]
fn apps_help_shows_scan_and_clean_subcommands() {
    let output = common::command::rebecca()
        .args(["apps", "--help"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("scan"));
    assert!(stdout.contains("clean"));
}

#[test]
fn apps_clean_help_lists_scan_cache_opt_out() {
    let output = common::command::rebecca()
        .args(["apps", "clean", "--help"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("--scan-cache"));
    assert!(stdout.contains("--no-scan-cache"));
}

#[test]
fn apps_scan_json_builds_app_leftovers_plan() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    let cache = local.join("Example App").join("Cache");
    let durable = local.join("Example App").join("Local Storage");
    write_fixture_file(cache.join("cache.bin"), b"abc");
    write_fixture_file(durable.join("state.bin"), b"keep");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args(["apps", "scan", "--format", "json", "--no-progress"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let envelope = common::support::api_envelope(&output.stdout);
    assert_eq!(envelope["command"], "apps scan");
    assert_eq!(envelope["payload_kind"], "app-leftovers-cleanup-plan");

    let value: serde_json::Value = envelope["data"].clone();
    assert_eq!(value["request"]["workflow"], "app-leftovers");
    assert_eq!(value["request"]["mode"], "dry-run");
    assert_eq!(value["summary"]["allowed_targets"], 1);
    assert_eq!(value["summary"]["estimated_bytes"], 3);

    let targets = value["targets"].as_array().unwrap();
    assert_eq!(targets.len(), 1);
    assert_eq!(targets[0]["rule_id"], "windows.app-leftover-local-cache");
    assert_eq!(targets[0]["status"], "allowed");
    assert!(
        PathBuf::from(targets[0]["path"].as_str().unwrap())
            .ends_with(Path::new("Example App").join("Cache"))
    );
    assert!(
        !targets[0]["path"]
            .as_str()
            .unwrap()
            .contains("Local Storage")
    );
}

#[test]
fn apps_scan_ndjson_uses_apps_scan_command_identity() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    write_fixture_file(
        local.join("Example App").join("Cache").join("cache.bin"),
        b"abc",
    );

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args(["apps", "scan", "--format", "ndjson", "--no-progress"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    let events = stdout
        .lines()
        .map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
        .collect::<Vec<_>>();

    assert_eq!(events.first().unwrap()["event_kind"], "started");
    assert_eq!(events.last().unwrap()["event_kind"], "completed");
    assert!(events.iter().all(|event| event["command"] == "apps scan"));
    assert_eq!(
        events.last().unwrap()["payload_kind"],
        "app-leftovers-cleanup-plan"
    );
}

#[test]
fn apps_scan_json_builds_wechat_leftovers_plan() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    write_app_leftover_fixture(&temp, "WeChat");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "WeChat")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args(["apps", "scan", "--format", "json", "--no-progress"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let envelope = common::support::api_envelope(&output.stdout);
    assert_eq!(envelope["command"], "apps scan");
    assert_eq!(envelope["payload_kind"], "app-leftovers-cleanup-plan");

    let value: serde_json::Value = envelope["data"].clone();
    assert_eq!(value["request"]["workflow"], "app-leftovers");
    assert_eq!(value["summary"]["allowed_targets"], 1);
    assert_eq!(value["summary"]["estimated_bytes"], 3);

    let targets = value["targets"].as_array().unwrap();
    assert_eq!(targets.len(), 1);
    assert_eq!(targets[0]["rule_id"], "windows.app-leftover-local-cache");
    assert_eq!(targets[0]["status"], "allowed");
    assert_eq!(targets[0]["estimate_source"], "fresh-scan");
    assert!(
        PathBuf::from(targets[0]["path"].as_str().unwrap())
            .ends_with(Path::new("WeChat").join("Cache"))
    );
    assert!(
        !targets[0]["path"]
            .as_str()
            .unwrap()
            .contains("Local Storage")
    );
}

#[cfg(windows)]
#[test]
fn apps_clean_yes_deletes_wechat_leftover_cache_contents() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    write_app_leftover_fixture(&temp, "WeChat");
    let cache_dir = local.join("WeChat").join("Cache");
    let durable_state = local.join("WeChat").join("Local Storage").join("state.bin");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "WeChat")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args([
            "apps",
            "clean",
            "--yes",
            "--format",
            "json",
            "--no-progress",
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    assert!(
        cache_dir.exists(),
        "app cache directory should be preserved"
    );
    assert_eq!(
        fs::read_dir(&cache_dir).unwrap().count(),
        0,
        "app cache directory should be emptied"
    );
    assert!(
        durable_state.exists(),
        "durable app state must remain untouched"
    );

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert_eq!(value["request"]["workflow"], "app-leftovers");
    assert_eq!(value["request"]["mode"], "recoverable-delete");
    assert_eq!(value["summary"]["completed_targets"], 1);
    assert_eq!(value["summary"]["blocked_targets"], 0);
    assert_eq!(value["execution_report"]["summary"]["completed_actions"], 1);
    assert_eq!(
        value["execution_report"]["summary"]["pending_reclaim_bytes"],
        3
    );
    assert_eq!(value["targets"].as_array().unwrap().len(), 1);
    assert_eq!(value["targets"][0]["status"], "completed");
    assert_eq!(
        value["targets"][0]["rule_id"],
        "windows.app-leftover-local-cache"
    );
}

#[cfg(windows)]
#[test]
fn apps_clean_yes_respects_exclude_path_during_execution() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    write_app_leftover_fixture(&temp, "WeChat");
    let code_cache_dir = local.join("WeChat").join("Code Cache");
    write_fixture_file(code_cache_dir.join("code.bin"), b"def");
    let cache_dir = local.join("WeChat").join("Cache");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "WeChat")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args([
            "apps",
            "clean",
            "--yes",
            "--format",
            "json",
            "--no-progress",
            "--exclude",
            cache_dir.to_str().unwrap(),
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    assert!(
        cache_dir.exists(),
        "excluded app cache directory should be preserved"
    );
    assert_eq!(fs::read_dir(&cache_dir).unwrap().count(), 1);
    assert!(
        code_cache_dir.exists(),
        "allowed app cache directory should still exist"
    );
    assert_eq!(
        fs::read_dir(&code_cache_dir).unwrap().count(),
        0,
        "allowed app cache directory should be emptied"
    );

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert_eq!(value["request"]["workflow"], "app-leftovers");
    assert_eq!(value["request"]["mode"], "recoverable-delete");
    assert_eq!(value["summary"]["total_targets"], 2);
    assert_eq!(value["summary"]["completed_targets"], 1);
    assert_eq!(value["summary"]["blocked_targets"], 1);
    assert_eq!(value["targets"].as_array().unwrap().len(), 2);
    assert!(value["targets"].as_array().unwrap().iter().any(|target| {
        target["status"] == "blocked"
            && target["reason_code"] == "safety-policy-blocked"
            && target["path"]
                .as_str()
                .unwrap()
                .contains("AppData/Local/WeChat/Cache")
    }));
    assert!(value["targets"].as_array().unwrap().iter().any(|target| {
        target["status"] == "completed"
            && target["rule_id"] == "windows.app-leftover-local-cache"
            && target["path"]
                .as_str()
                .unwrap()
                .contains("AppData/Local/WeChat/Code Cache")
    }));
}

#[test]
fn apps_scan_json_honors_exclude_flag() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    let cache = local.join("Example App").join("Cache");
    write_fixture_file(cache.join("cache.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args([
            "apps",
            "scan",
            "--format",
            "json",
            "--no-progress",
            "--exclude",
            cache.to_str().unwrap(),
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert_eq!(value["summary"]["allowed_targets"], 0);
    assert_eq!(value["summary"]["blocked_targets"], 1);

    let blocked = &value["targets"].as_array().unwrap()[0];
    assert_eq!(blocked["status"], "blocked");
    assert_eq!(blocked["reason_code"], "safety-policy-blocked");
    assert!(
        blocked["reason"]
            .as_str()
            .unwrap()
            .contains("user-protected path")
    );
}

#[test]
fn apps_scan_human_output_names_app_leftovers_workflow() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    write_fixture_file(
        local.join("Example App").join("Cache").join("cache.bin"),
        b"abc",
    );

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args(["apps", "scan", "--no-progress"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Workflow: App leftovers"));
    assert!(stdout.contains("Cleanup mode: dry-run"));
    assert!(stdout.contains("windows.app-leftover-local-cache"));
}

#[test]
fn apps_clean_defaults_to_preview_without_deleting() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    let cache_file = local.join("Example App").join("Cache").join("cache.bin");
    write_fixture_file(&cache_file, b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args(["apps", "clean", "--format", "json", "--no-progress"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    assert!(cache_file.exists(), "apps clean should preview by default");

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert_eq!(value["request"]["workflow"], "app-leftovers");
    assert_eq!(value["request"]["mode"], "dry-run");
    assert_eq!(value["summary"]["allowed_targets"], 1);

    let scan_cache_dir = temp.path().join("rebecca-cache").join("scan");
    let cache_entries = fs::read_dir(scan_cache_dir).unwrap().count();
    assert_eq!(cache_entries, 1);
}

#[test]
fn apps_clean_no_scan_cache_disables_preview_cache_writes() {
    let temp = tempfile::tempdir().unwrap();
    let (local, roaming) = appdata_roots(&temp);
    let cache_file = local.join("Example App").join("Cache").join("cache.bin");
    write_fixture_file(&cache_file, b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args([
            "apps",
            "clean",
            "--format",
            "json",
            "--no-progress",
            "--no-scan-cache",
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    assert!(cache_file.exists(), "apps clean should preview by default");

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert_eq!(value["request"]["workflow"], "app-leftovers");
    assert_eq!(value["request"]["mode"], "dry-run");
    assert_eq!(value["summary"]["allowed_targets"], 1);
    assert!(!temp.path().join("rebecca-cache").join("scan").exists());
}

#[test]
fn apps_clean_dry_run_blocks_rebecca_owned_storage_overlap() {
    let temp = tempfile::tempdir().unwrap();
    let local = temp
        .path()
        .join("rebecca-cache")
        .join("AppData")
        .join("Local");
    let roaming = temp.path().join("AppData").join("Roaming");
    let cache_file = local.join("Example App").join("Cache").join("cache.bin");
    write_fixture_file(&cache_file, b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_STEAM_DISCOVERY", "none")
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .args([
            "apps",
            "clean",
            "--dry-run",
            "--format",
            "json",
            "--no-progress",
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    assert!(cache_file.exists(), "dry-run must not delete files");

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert_eq!(value["summary"]["allowed_targets"], 0);
    assert_eq!(value["summary"]["blocked_targets"], 1);
    assert!(
        value["summary"]["issue_matrix"]
            .as_array()
            .unwrap()
            .iter()
            .any(|issue| issue["status"] == "blocked"
                && issue["reason_code"] == "safety-policy-blocked"
                && issue["targets"] == 1)
    );

    let blocked = &value["targets"].as_array().unwrap()[0];
    assert_eq!(blocked["status"], "blocked");
    assert_eq!(blocked["reason_code"], "safety-policy-blocked");
    assert!(
        blocked["reason"]
            .as_str()
            .unwrap()
            .contains("Rebecca-owned Cache dir")
    );
}

#[test]
fn apps_scan_empty_inventory_reports_empty_plan() {
    let temp = tempfile::tempdir().unwrap();

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_APP_DISCOVERY", "none")
        .args(["apps", "scan", "--format", "json", "--no-progress"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert_eq!(value["request"]["workflow"], "app-leftovers");
    assert_eq!(value["summary"]["total_targets"], 0);
    assert_eq!(value["summary"]["allowed_targets"], 0);
}
