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 write_node_project(dir: impl AsRef<Path>) {
    write_fixture_file(dir.as_ref().join("package.json"), b"{}");
}

fn write_rust_project(dir: impl AsRef<Path>) {
    write_fixture_file(dir.as_ref().join("Cargo.toml"), b"[package]");
}

fn npm_cache_rule_fixture(temp: &tempfile::TempDir) -> Option<(PathBuf, PathBuf, &'static str)> {
    if cfg!(windows) {
        let root = temp.path().join("local").join("npm-cache");
        let target = root.join("_cacache");
        Some((root, target, "windows.npm-cache"))
    } else if cfg!(target_os = "linux") {
        let root = temp.path().join(".npm");
        let target = root.join("_cacache");
        Some((root, target, "linux.npm-cache"))
    } else if cfg!(target_os = "macos") {
        let root = temp.path().join(".npm");
        let target = root.join("_cacache");
        Some((root, target, "macos.npm-cache"))
    } else {
        None
    }
}

fn ndjson_events(stdout: &[u8]) -> Vec<serde_json::Value> {
    String::from_utf8_lossy(stdout)
        .lines()
        .map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
        .collect()
}

fn assert_monotonic_sequences(events: &[serde_json::Value]) {
    assert!(events.windows(2).all(|pair| {
        pair[0]["sequence"].as_u64().unwrap() < pair[1]["sequence"].as_u64().unwrap()
    }));
}

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

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("space"));
    assert!(stdout.contains("map"));
    assert!(stdout.contains("artifacts"));
    assert!(stdout.contains("lint"));
}

#[test]
fn inspect_space_json_reports_top_entries_and_diagnostics() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    let missing = temp.path().join("missing");
    write_fixture_file(root.join("zeta").join("data.bin"), b"abc");
    write_fixture_file(root.join("alpha").join("data.bin"), b"abc");
    write_fixture_file(root.join("small.txt"), b"x");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "space",
            "--format",
            "json",
            "--root",
            root.to_str().unwrap(),
            "--root",
            missing.to_str().unwrap(),
            "--top",
            "2",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    assert_eq!(envelope["command"], "inspect space");
    assert_eq!(envelope["payload_kind"], "inspect-space");

    let value = &envelope["data"];
    assert_eq!(value["totals"]["estimated_bytes"], 7);
    assert_eq!(value["totals"]["files"], 3);
    assert_eq!(value["top_entries"].as_array().unwrap().len(), 2);
    assert_eq!(
        PathBuf::from(value["top_entries"][0]["path"].as_str().unwrap()),
        root.join("alpha")
    );
    assert_eq!(
        PathBuf::from(value["top_entries"][1]["path"].as_str().unwrap()),
        root.join("zeta")
    );
    assert_eq!(value["top_entries"][0]["estimate_source"], "fresh-scan");
    assert_eq!(
        value["top_entries"][0]["estimate_backend"],
        "portable-recursive"
    );
    assert_eq!(value["top_entries"][0]["estimate_confidence"], "exact");
    assert_eq!(value["diagnostic_summary"]["total"], 1);
    assert_eq!(value["diagnostic_summary"]["retained"], 1);
    assert_eq!(value["diagnostic_summary"]["truncated"], 0);
    assert_eq!(
        value["diagnostic_summary"]["by_kind"][0]["kind"],
        "root-missing"
    );
    assert_eq!(value["diagnostic_summary"]["by_kind"][0]["count"], 1);
    assert_eq!(value["diagnostics"][0]["kind"], "root-missing");
}

#[test]
fn inspect_space_json_diagnostic_limit_zero_keeps_summary_only() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    let missing = temp.path().join("missing");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "space",
            "--format",
            "json",
            "--root",
            root.to_str().unwrap(),
            "--root",
            missing.to_str().unwrap(),
            "--diagnostic-limit",
            "0",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let value = &envelope["data"];
    assert_eq!(value["diagnostic_summary"]["total"], 1);
    assert_eq!(value["diagnostic_summary"]["retained"], 0);
    assert_eq!(value["diagnostic_summary"]["truncated"], 1);
    assert_eq!(
        value["diagnostic_summary"]["by_kind"][0]["kind"],
        "root-missing"
    );
    assert!(value["diagnostics"].as_array().unwrap().is_empty());
}

#[test]
fn inspect_space_human_diagnostic_limit_zero_keeps_summary_count() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    let missing = temp.path().join("missing");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "space",
            "--root",
            root.to_str().unwrap(),
            "--root",
            missing.to_str().unwrap(),
            "--diagnostic-limit",
            "0",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Diagnostics: 1"));
    assert!(stdout.contains("Space diagnostics: 1 observation"));
    assert!(stdout.contains("  - root-missing: 1 observation"));
    assert!(stdout.contains("  - truncated: 1 observation not shown"));
    assert!(!stdout.contains("Space diagnostic samples:"));
}

#[test]
fn inspect_space_json_accepts_scan_backend_and_reports_provenance() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("target").join("app.bin"), b"abcd");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "space",
            "--format",
            "json",
            "--scan-backend",
            "windows-ntfs-mft-experimental",
            "--root",
            root.to_str().unwrap(),
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let entry = &envelope["data"]["top_entries"][0];
    assert_eq!(entry["estimate_source"], "fresh-scan");
    assert!(entry["estimate_backend"].is_string());
    assert_eq!(entry["estimate_confidence"], "exact");
    if let Some(reason) = entry["estimate_fallback_reason"].as_str() {
        assert!(reason.contains("windows-ntfs-mft-experimental"));
        assert_eq!(
            entry["estimate_caveats"][0]["code"],
            "experimental-ntfs-mft-fallback"
        );
    } else {
        assert_eq!(entry["estimate_backend"], "windows-ntfs-mft-experimental");
        assert!(
            entry["estimate_backend_source"]
                .as_str()
                .is_some_and(|source| source.starts_with("windows-ntfs-mft-experimental-")),
            "entry should include the live NTFS/MFT source when no fallback occurs: {entry:#}"
        );
    }
}

#[test]
fn inspect_map_json_reports_ranked_entries_and_fallback_provenance() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("zeta").join("data.bin"), b"abc");
    write_fixture_file(root.join("alpha").join("data.bin"), b"abc");
    write_fixture_file(root.join("small.txt"), b"x");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_TEST_DISABLE_LIVE_NTFS_MFT", "1")
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "windows-ntfs-mft-experimental",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "2",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    assert_eq!(envelope["command"], "inspect map");
    assert_eq!(envelope["payload_kind"], "inspect-map");

    let value = &envelope["data"];
    assert_eq!(value["totals"]["logical_bytes"], 7);
    if cfg!(unix) {
        assert!(
            value["totals"]["allocated_bytes"]
                .as_u64()
                .is_some_and(|bytes| bytes >= 7)
        );
    } else {
        assert_eq!(value["totals"]["allocated_bytes"], serde_json::Value::Null);
    }
    assert_eq!(value["totals"]["files"], 3);
    assert_eq!(value["top_entries"].as_array().unwrap().len(), 2);
    assert_eq!(
        PathBuf::from(value["top_entries"][0]["path"].as_str().unwrap()),
        root.join("alpha")
    );
    assert_eq!(value["top_entries"][0]["depth"], 1);
    assert_eq!(value["top_entries"][0]["estimate_source"], "fresh-scan");
    assert_eq!(
        value["top_entries"][0]["estimate_backend"],
        "portable-recursive"
    );
    assert_eq!(value["top_entries"][0]["estimate_confidence"], "exact");
    assert!(
        value["top_entries"][0]["estimate_fallback_reason"]
            .as_str()
            .is_some_and(|reason| reason.contains("windows-ntfs-mft-experimental"))
    );
    assert_eq!(value["diagnostic_summary"]["total"], 1);
    assert_eq!(value["diagnostic_summary"]["retained"], 1);
    assert_eq!(value["diagnostic_summary"]["truncated"], 0);
    assert_eq!(
        value["diagnostic_summary"]["by_kind"][0]["kind"],
        "fallback"
    );
    assert_eq!(value["diagnostic_summary"]["by_kind"][0]["count"], 1);
    assert_eq!(value["diagnostics"][0]["kind"], "fallback");
}

#[test]
fn inspect_map_json_reports_requested_groups() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("src").join("main.rs"), b"abcd");
    write_fixture_file(root.join("readme.md"), b"xy");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "0",
            "--group-by",
            "type",
            "--group-by",
            "extension",
            "--group-by",
            "depth",
            "--group-by",
            "age",
            "--group-limit",
            "10",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let value = &envelope["data"];
    assert!(value["top_entries"].as_array().unwrap().is_empty());
    assert_json_group(value, "type", "file", 6, 2);
    assert_json_group_directories(value, "type", "directory", 1);
    assert_json_group(value, "extension", ".rs", 4, 1);
    assert_json_group(value, "extension", ".md", 2, 1);
    assert_json_group(value, "depth", "depth-1", 2, 1);
    assert_json_group(value, "depth", "depth-2", 4, 1);
    assert_json_group(value, "age", "modified-7d", 6, 2);
}

#[test]
fn inspect_map_json_respects_requested_sort_fields() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("many").join("a.txt"), b"x");
    write_fixture_file(root.join("many").join("b.txt"), b"x");
    write_fixture_file(root.join("large.bin"), b"abcdefghij");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "2",
            "--sort",
            "files",
            "--group-by",
            "extension",
            "--group-limit",
            "2",
            "--group-sort",
            "files",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let value = &envelope["data"];
    assert_eq!(
        PathBuf::from(value["top_entries"][0]["path"].as_str().unwrap()),
        root.join("many")
    );
    assert_eq!(value["top_entries"][0]["files"], 2);
    assert_eq!(value["groups"][0]["key"], ".txt");
    assert_eq!(value["groups"][0]["metrics"]["files"], 2);
    assert_eq!(value["groups"][1]["key"], ".bin");
    assert_eq!(value["groups"][1]["metrics"]["logical_bytes"], 10);
}

#[test]
fn inspect_map_json_filters_ranked_entries_without_changing_totals() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("LargeCache.bin"), b"abcdef");
    write_fixture_file(root.join("small-cache.bin"), b"x");
    write_fixture_file(root.join("LargeCache").join("nested.bin"), b"xyz");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--min-logical-bytes",
            "4",
            "--entry-kind",
            "file",
            "--path-contains",
            "largecache",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let value = &envelope["data"];
    assert_eq!(value["totals"]["logical_bytes"], 10);
    assert_eq!(value["totals"]["files"], 3);
    let entries = value["top_entries"].as_array().unwrap();
    assert_eq!(entries.len(), 1);
    assert_eq!(
        PathBuf::from(entries[0]["path"].as_str().unwrap()),
        root.join("LargeCache.bin")
    );
    assert_eq!(entries[0]["kind"], "file");
    assert_eq!(entries[0]["logical_bytes"], 6);
}

#[test]
fn inspect_map_table_uses_ranked_entry_filters() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("LargeCache").join("nested.bin"), b"abcdef");
    write_fixture_file(root.join("other").join("nested.bin"), b"xyz");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--table",
            "csv",
            "--table-row",
            "entry",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--max-depth",
            "1",
            "--entry-kind",
            "directory",
            "--path-contains",
            "largecache",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 2);
    assert_eq!(lines[0], INSPECT_MAP_TABLE_HEADER_CSV);
    assert!(lines[1].starts_with("entry,1,"));
    assert!(lines[1].contains(&root.join("LargeCache").display().to_string()));
    assert!(!lines[1].contains("other"));
}

#[test]
fn inspect_map_json_reports_cleanup_advice_for_rule_targets() {
    let temp = tempfile::tempdir().unwrap();
    let Some((root, target, rule_id)) = npm_cache_rule_fixture(&temp) else {
        return;
    };
    write_fixture_file(target.join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--max-depth",
            "1",
            "--entry-kind",
            "directory",
            "--path-contains",
            "_cacache",
            "--cleanup-advice",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let entries = envelope["data"]["top_entries"].as_array().unwrap();
    let entry = entries
        .iter()
        .find(|entry| Path::new(entry["path"].as_str().unwrap()) == target.as_path())
        .expect("npm cache entry should be present");
    let advice = &entry["cleanup_advice"];
    assert_eq!(advice["status"], "maybe-cleanable");
    assert_eq!(advice["source"], "cleanup-rule");
    assert_eq!(advice["relation"], "exact");
    assert_eq!(advice["rule_id"], rule_id);
    assert_eq!(advice["category"], "development");
    assert_eq!(advice["safety_level"], "moderate");
    assert_eq!(advice["required_flags"][0], "--allow-moderate");
    assert_eq!(advice["evidence"][0]["status"], advice["status"]);
    assert_eq!(advice["evidence"][0]["source"], advice["source"]);
    assert_eq!(advice["evidence"][0]["relation"], advice["relation"]);
    assert_eq!(advice["evidence"][0]["rule_id"], advice["rule_id"]);
    assert_eq!(
        advice["evidence"][0]["required_flags"],
        advice["required_flags"]
    );
    assert_eq!(advice["suggested_command"]["command"], "rebecca");
    assert_eq!(
        advice["suggested_command"]["args"],
        serde_json::json!(["clean", "--dry-run", "--rule", rule_id])
    );
}

#[test]
fn inspect_map_human_summarizes_cleanup_advice_and_next_command() {
    let temp = tempfile::tempdir().unwrap();
    let Some((root, target, rule_id)) = npm_cache_rule_fixture(&temp) else {
        return;
    };
    write_fixture_file(target.join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--max-depth",
            "1",
            "--entry-kind",
            "directory",
            "--path-contains",
            "_cacache",
            "--cleanup-advice",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Cleanup advice summary:"));
    assert!(stdout.contains("- maybe-cleanable: 1 entry, 6 (6 B)"));
    assert!(stdout.contains("Suggested cleanup commands:"));
    assert!(stdout.contains(&format!(
        "rebecca clean --dry-run --rule {rule_id} --allow-moderate"
    )));
    assert!(
        stdout
            .contains("Cleanup advice is read-only; rerun a suggested command to preview cleanup.")
    );
}

#[test]
fn inspect_map_json_reports_project_artifact_cleanup_advice() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    let target = root.join("node_modules");
    write_fixture_file(root.join("package.json"), b"{}");
    write_fixture_file(target.join(".cache").join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--entry-kind",
            "directory",
            "--path-contains",
            "node_modules",
            "--cleanup-advice",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let entries = envelope["data"]["top_entries"].as_array().unwrap();
    let entry = entries
        .iter()
        .find(|entry| Path::new(entry["path"].as_str().unwrap()) == target.as_path())
        .expect("node_modules entry should be present");
    let advice = &entry["cleanup_advice"];
    assert_eq!(advice["status"], "maybe-cleanable");
    assert_eq!(advice["source"], "project-artifact");
    assert_eq!(advice["rule_id"], "portable.project-artifact-node-modules");
    assert_eq!(advice["category"], "project-artifact");
    assert_eq!(advice["required_flags"][0], "--min-age-days 0");
    assert_eq!(advice["suggested_command"]["args"][0], "purge");
    assert_eq!(advice["suggested_command"]["args"][4], "--artifact");
    assert_eq!(advice["suggested_command"]["args"][5], "node_modules");
}

#[test]
fn inspect_map_json_reports_app_leftover_cleanup_advice() {
    let temp = tempfile::tempdir().unwrap();
    let local = temp.path().join("AppData").join("Local");
    let roaming = temp.path().join("AppData").join("Roaming");
    let app_root = local.join("Example App");
    let target = app_root.join("Cache");
    write_fixture_file(target.join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .env("USERPROFILE", temp.path())
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "portable-recursive",
            "--root",
            app_root.to_str().unwrap(),
            "--top",
            "10",
            "--max-depth",
            "1",
            "--entry-kind",
            "directory",
            "--path-contains",
            "Cache",
            "--cleanup-advice",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let entries = envelope["data"]["top_entries"].as_array().unwrap();
    let entry = entries
        .iter()
        .find(|entry| Path::new(entry["path"].as_str().unwrap()) == target.as_path())
        .expect("app cache entry should be present");
    let advice = &entry["cleanup_advice"];
    assert_eq!(advice["status"], "cleanable");
    assert_eq!(advice["source"], "app-leftover");
    assert_eq!(advice["rule_id"], "windows.app-leftover-local-cache");
    assert_eq!(advice["category"], "app-leftover");
    assert_eq!(
        advice["suggested_command"]["args"],
        serde_json::json!(["apps", "clean", "--dry-run"])
    );
    assert_eq!(
        advice["app_leftover"]["app"]["stable_id"],
        "debug/installed-application/0"
    );
    assert_eq!(advice["app_leftover"]["app"]["display_name"], "Example App");
    assert_eq!(advice["app_leftover"]["source"], "local-app-data");
    assert_eq!(advice["app_leftover"]["target_leaf"], "Cache");
    assert_eq!(
        advice["app_leftover"]["deletion_style"],
        "preserve-root-contents"
    );
}

#[test]
fn inspect_map_table_appends_cleanup_advice_columns_when_enabled() {
    let temp = tempfile::tempdir().unwrap();
    let Some((root, target, rule_id)) = npm_cache_rule_fixture(&temp) else {
        return;
    };
    write_fixture_file(target.join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--table",
            "csv",
            "--table-row",
            "entry",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--entry-kind",
            "directory",
            "--path-contains",
            "_cacache",
            "--cleanup-advice",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 2);
    assert_eq!(lines[0], INSPECT_MAP_TABLE_HEADER_WITH_ADVICE_CSV);
    assert!(lines[1].contains("maybe-cleanable"));
    assert!(lines[1].contains("cleanup-rule"));
    assert!(lines[1].contains(rule_id));
    assert!(lines[1].contains("--allow-moderate"));
    assert!(lines[1].contains(&format!("rebecca clean --dry-run --rule {rule_id}")));
}

#[test]
fn inspect_map_table_reports_app_leftover_advice_columns() {
    let temp = tempfile::tempdir().unwrap();
    let local = temp.path().join("AppData").join("Local");
    let roaming = temp.path().join("AppData").join("Roaming");
    let app_root = local.join("Example App");
    let target = app_root.join("Cache");
    write_fixture_file(target.join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_INSTALLED_APPLICATIONS", "Example App")
        .env("LOCALAPPDATA", &local)
        .env("APPDATA", &roaming)
        .env("USERPROFILE", temp.path())
        .args([
            "inspect",
            "map",
            "--table",
            "csv",
            "--table-row",
            "entry",
            "--scan-backend",
            "portable-recursive",
            "--root",
            app_root.to_str().unwrap(),
            "--top",
            "10",
            "--max-depth",
            "1",
            "--entry-kind",
            "directory",
            "--path-contains",
            "Cache",
            "--cleanup-advice",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 2);
    assert_eq!(lines[0], INSPECT_MAP_TABLE_HEADER_WITH_ADVICE_CSV);
    assert!(lines[1].contains("cleanable"));
    assert!(lines[1].contains("app-leftover"));
    assert!(lines[1].contains("windows.app-leftover-local-cache"));
    assert!(lines[1].contains("rebecca apps clean --dry-run"));
    assert!(lines[1].contains("debug/installed-application/0"));
    assert!(lines[1].contains("Example App"));
    assert!(lines[1].contains("local-app-data"));
    assert!(lines[1].contains("preserve-root-contents"));
}

#[test]
fn inspect_map_table_quotes_cleanup_command_arguments_with_spaces_and_quotes() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace with 'quote'");
    write_fixture_file(root.join("package.json"), b"{}");
    write_fixture_file(root.join("node_modules").join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--table",
            "csv",
            "--table-row",
            "entry",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--entry-kind",
            "directory",
            "--path-contains",
            "node_modules",
            "--cleanup-advice",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 2);
    assert_eq!(lines[0], INSPECT_MAP_TABLE_HEADER_WITH_ADVICE_CSV);
    let quoted_root = root.display().to_string().replace('\'', "''");
    assert!(lines[1].contains(&format!(
        "rebecca purge --dry-run --root '{}' --artifact node_modules",
        quoted_root
    )));
}

#[test]
fn inspect_map_table_csv_exports_flat_rows() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace,with-comma");
    write_fixture_file(root.join("many,files").join("a.txt"), b"x");
    write_fixture_file(root.join("many,files").join("b.txt"), b"x");
    write_fixture_file(root.join("large.bin"), b"abcdefghij");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--table",
            "csv",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "1",
            "--sort",
            "files",
            "--group-by",
            "extension",
            "--group-limit",
            "1",
            "--group-sort",
            "files",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 5);
    assert_eq!(lines[0], INSPECT_MAP_TABLE_HEADER_CSV);
    assert!(lines[1].starts_with("total,"));
    assert!(lines[2].starts_with("root,"));
    assert!(lines[3].starts_with("entry,1,"));
    assert!(lines[4].starts_with("group,1,"));
    assert!(lines[2].contains(&format!("\"{}\"", root.display())));
    assert!(lines[3].contains(&format!("\"{}\"", root.join("many,files").display())));
    assert!(lines[4].contains(",extension,.txt,.txt,"));
    assert!(!stdout.contains('{'));
}

#[test]
fn inspect_map_table_tsv_exports_tab_separated_rows() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("src").join("main.rs"), b"abcd");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--table",
            "tsv",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "1",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines[0], INSPECT_MAP_TABLE_HEADER_TSV);
    assert_eq!(lines[1].split('\t').next().unwrap(), "total");
    assert_eq!(lines[2].split('\t').next().unwrap(), "root");
    assert_eq!(lines[3].split('\t').next().unwrap(), "entry");
    assert!(!stdout.contains('{'));
}

#[test]
fn inspect_map_table_row_filters_selected_kinds() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("many").join("a.txt"), b"x");
    write_fixture_file(root.join("many").join("b.txt"), b"x");
    write_fixture_file(root.join("large.bin"), b"abcdefghij");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--table",
            "csv",
            "--table-row",
            "entry",
            "--table-row",
            "group",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "1",
            "--sort",
            "files",
            "--group-by",
            "extension",
            "--group-limit",
            "1",
            "--group-sort",
            "files",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 3);
    assert_eq!(lines[0], INSPECT_MAP_TABLE_HEADER_CSV);
    assert!(lines[1].starts_with("entry,1,"));
    assert!(lines[2].starts_with("group,1,"));
    let row_kinds = lines
        .iter()
        .skip(1)
        .map(|line| line.split(',').next().unwrap())
        .collect::<Vec<_>>();
    assert_eq!(row_kinds, ["entry", "group"]);
}

#[test]
fn inspect_map_table_rejects_machine_format() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--table",
            "csv",
            "--root",
            root.to_str().unwrap(),
        ])
        .output()
        .unwrap();

    assert!(!output.status.success());
    assert!(common::support::stderr(&output).contains("--table"));
}

#[test]
fn inspect_map_table_row_requires_table_output() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--table-row",
            "entry",
            "--root",
            root.to_str().unwrap(),
        ])
        .output()
        .unwrap();

    assert!(!output.status.success());
    assert!(common::support::stderr(&output).contains("--table-row"));
}

fn assert_json_group(
    value: &serde_json::Value,
    kind: &str,
    key: &str,
    logical_bytes: u64,
    files: u64,
) {
    let group = value["groups"]
        .as_array()
        .unwrap()
        .iter()
        .find(|group| group["kind"] == kind && group["key"] == key)
        .unwrap_or_else(|| panic!("missing group {kind}:{key}"));
    assert_eq!(group["metrics"]["logical_bytes"], logical_bytes);
    assert_eq!(group["metrics"]["files"], files);
}

fn assert_json_group_directories(
    value: &serde_json::Value,
    kind: &str,
    key: &str,
    directories: u64,
) {
    let group = value["groups"]
        .as_array()
        .unwrap()
        .iter()
        .find(|group| group["kind"] == kind && group["key"] == key)
        .unwrap_or_else(|| panic!("missing group {kind}:{key}"));
    assert_eq!(group["metrics"]["logical_bytes"], 0);
    assert_eq!(group["metrics"]["files"], 0);
    assert_eq!(group["metrics"]["directories"], directories);
}

const INSPECT_MAP_TABLE_HEADER_CSV: &str = "row_kind,rank,path,root,status,entry_kind,group_kind,group_key,group_label,depth,logical_bytes,allocated_bytes,unique_logical_bytes,unique_allocated_bytes,files,directories,estimate_source,estimate_backend,estimate_backend_source,estimate_confidence,estimate_fallback_reason,estimate_caveats,reason";
const INSPECT_MAP_TABLE_HEADER_WITH_ADVICE_CSV: &str = "row_kind,rank,path,root,status,entry_kind,group_kind,group_key,group_label,depth,logical_bytes,allocated_bytes,unique_logical_bytes,unique_allocated_bytes,files,directories,estimate_source,estimate_backend,estimate_backend_source,estimate_confidence,estimate_fallback_reason,estimate_caveats,reason,cleanup_status,cleanup_relation,cleanup_source,cleanup_rule_id,cleanup_category,cleanup_safety_level,cleanup_required_flags,cleanup_required_warnings,cleanup_protection_kind,cleanup_matched_path,cleanup_reason,cleanup_command,cleanup_app_stable_id,cleanup_app_display_name,cleanup_app_publisher,cleanup_app_leftover_source,cleanup_app_leftover_target_leaf,cleanup_app_leftover_deletion_style,cleanup_app_leftover_modified_at_unix_seconds";
const INSPECT_MAP_TABLE_HEADER_TSV: &str = "row_kind\trank\tpath\troot\tstatus\tentry_kind\tgroup_kind\tgroup_key\tgroup_label\tdepth\tlogical_bytes\tallocated_bytes\tunique_logical_bytes\tunique_allocated_bytes\tfiles\tdirectories\testimate_source\testimate_backend\testimate_backend_source\testimate_confidence\testimate_fallback_reason\testimate_caveats\treason";

#[cfg(windows)]
#[test]
fn inspect_map_json_windows_native_reports_native_provenance() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("alpha").join("data.bin"), b"abcd");
    write_fixture_file(root.join("beta.bin"), b"xyz");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "windows-native",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let value = &envelope["data"];
    assert_eq!(value["totals"]["logical_bytes"], 7);
    assert!(
        value["totals"]["allocated_bytes"]
            .as_u64()
            .is_some_and(|bytes| bytes >= 7)
    );
    assert_eq!(value["totals"]["files"], 2);
    assert_eq!(value["totals"]["directories"], 1);
    assert!(
        value["top_entries"][0]["allocated_bytes"]
            .as_u64()
            .is_some_and(
                |bytes| bytes >= value["top_entries"][0]["logical_bytes"].as_u64().unwrap()
            )
    );
    assert_eq!(
        value["top_entries"][0]["estimate_backend"],
        "windows-native"
    );
    assert_eq!(value["top_entries"][0]["estimate_confidence"], "exact");
    assert!(value["top_entries"][0]["estimate_fallback_reason"].is_null());
    assert!(
        value["diagnostics"]
            .as_array()
            .is_some_and(|diagnostics| diagnostics.is_empty())
    );
}

#[cfg(windows)]
#[test]
fn inspect_map_json_windows_native_reports_hardlink_caveat() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    let original = root.join("original.bin");
    let linked = root.join("linked.bin");
    write_fixture_file(&original, b"abcd");
    fs::hard_link(&original, &linked).unwrap();

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "windows-native",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let value = &envelope["data"];
    assert_eq!(value["totals"]["logical_bytes"], 8);
    assert_eq!(value["totals"]["unique_logical_bytes"], 4);
    assert_eq!(value["roots"][0]["metrics"]["unique_logical_bytes"], 4);
    let allocated_bytes = value["totals"]["allocated_bytes"]
        .as_u64()
        .expect("native hardlink fixture should report path-ranked allocated bytes");
    let unique_allocated_bytes = value["totals"]["unique_allocated_bytes"]
        .as_u64()
        .expect("native hardlink fixture should report unique allocated bytes");
    assert!(
        allocated_bytes >= unique_allocated_bytes,
        "path-ranked allocation should be at least unique allocation"
    );
    assert!(
        value["roots"][0]["estimate_caveats"]
            .as_array()
            .unwrap()
            .iter()
            .any(|caveat| caveat["code"] == "hardlink-file")
    );
    assert!(
        value["top_entries"]
            .as_array()
            .unwrap()
            .iter()
            .any(|entry| entry["estimate_caveats"]
                .as_array()
                .unwrap()
                .iter()
                .any(|caveat| caveat["code"] == "hardlink-file"))
    );
}

#[test]
fn inspect_map_json_top_zero_preserves_totals_without_entries() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("large.bin"), b"abc");
    write_fixture_file(root.join("small.bin"), b"x");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "0",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    assert_eq!(envelope["payload_kind"], "inspect-map");
    assert_eq!(envelope["data"]["totals"]["logical_bytes"], 4);
    assert!(
        envelope["data"]["top_entries"]
            .as_array()
            .unwrap()
            .is_empty()
    );
}

#[test]
fn inspect_map_human_ranks_entries_with_share_and_bars() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("large.bin"), b"abcdefgh");
    write_fixture_file(root.join("small.bin"), b"xy");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "2",
            "--entry-kind",
            "file",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Top map entries:"));
    assert!(stdout.contains("  #1 "));
    assert!(stdout.contains("8 bytes (8 B)"));
    assert!(stdout.contains("80.0%"));
    assert!(stdout.contains("[################----]"));
    assert!(stdout.contains("large.bin"));
}

#[test]
fn inspect_map_human_screen_reader_omits_visual_bars() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("large.bin"), b"abcdefgh");
    write_fixture_file(root.join("small.bin"), b"xy");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--screen-reader",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "2",
            "--entry-kind",
            "file",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Top map entries (screen-reader):"));
    assert!(stdout.contains("#1: 8 bytes (8 B); share 80.0%;"));
    assert!(stdout.contains("kind file; depth 1; 1 file; 0 directories"));
    assert!(!stdout.contains("[################----]"));
}

#[test]
fn inspect_map_human_compacts_very_long_paths() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    let long_path = root
        .join("very-long-directory-name-for-human-map-rendering")
        .join("another-long-directory-name-for-human-map-rendering")
        .join("yet-another-long-directory-name-for-human-map-rendering")
        .join("final-large-cache-entry.bin");
    write_fixture_file(&long_path, b"abcdefgh");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "1",
            "--entry-kind",
            "file",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("..."));
    assert!(stdout.contains("final-large-cache-entry.bin"));
}

#[test]
fn inspect_map_human_full_path_disables_compaction() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    let long_path = root
        .join("very-long-directory-name-for-human-map-rendering")
        .join("another-long-directory-name-for-human-map-rendering")
        .join("yet-another-long-directory-name-for-human-map-rendering")
        .join("final-large-cache-entry.bin");
    write_fixture_file(&long_path, b"abcdefgh");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--full-path",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "1",
            "--entry-kind",
            "file",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains(&long_path.display().to_string()));
    assert!(!stdout.contains("..."));
}

#[test]
fn inspect_map_human_no_bars_hides_visual_bars() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("large.bin"), b"abcdefgh");
    write_fixture_file(root.join("small.bin"), b"xy");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--no-bars",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "2",
            "--entry-kind",
            "file",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Top map entries:"));
    assert!(stdout.contains("80.0%"));
    assert!(stdout.contains("large.bin"));
    assert!(!stdout.contains("[################----]"));
}

#[test]
fn inspect_map_human_bar_width_controls_visual_bars() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("large.bin"), b"abcdefgh");
    write_fixture_file(root.join("small.bin"), b"xy");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--bar-width",
            "8",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "2",
            "--entry-kind",
            "file",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("[######--]"));
    assert!(!stdout.contains("[################----]"));
}

#[test]
fn inspect_map_human_groups_show_rank_share_and_bars() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("large.bin"), b"abcdefgh");
    write_fixture_file(root.join("small.txt"), b"xy");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "0",
            "--group-by",
            "extension",
            "--group-limit",
            "2",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Map groups:"));
    assert!(stdout.contains("  #1 "));
    assert!(stdout.contains("80.0%"));
    assert!(stdout.contains("[################----]"));
    assert!(stdout.contains(".bin [extension]"));
}

#[test]
fn inspect_map_json_diagnostic_limit_zero_keeps_summary_only() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_TEST_DISABLE_LIVE_NTFS_MFT", "1")
        .args([
            "inspect",
            "map",
            "--format",
            "json",
            "--scan-backend",
            "windows-ntfs-mft-experimental",
            "--root",
            root.to_str().unwrap(),
            "--diagnostic-limit",
            "0",
        ])
        .output()
        .unwrap();

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

    let envelope = common::support::api_envelope(&output.stdout);
    let value = &envelope["data"];
    assert_eq!(value["diagnostic_summary"]["total"], 1);
    assert_eq!(value["diagnostic_summary"]["retained"], 0);
    assert_eq!(value["diagnostic_summary"]["truncated"], 1);
    assert!(value["diagnostics"].as_array().unwrap().is_empty());
}

#[test]
fn inspect_map_human_reports_diagnostic_summary_and_samples() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_TEST_DISABLE_LIVE_NTFS_MFT", "1")
        .args([
            "inspect",
            "map",
            "--scan-backend",
            "windows-ntfs-mft-experimental",
            "--root",
            root.to_str().unwrap(),
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Diagnostics: 1"));
    assert!(stdout.contains("Disk map diagnostics: 1 observation"));
    assert!(stdout.contains("  - fallback: 1 observation"));
    assert!(stdout.contains("Disk map diagnostic samples: 1 observation"));
}

#[test]
fn inspect_map_human_diagnostic_limit_zero_keeps_summary_count() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_TEST_DISABLE_LIVE_NTFS_MFT", "1")
        .args([
            "inspect",
            "map",
            "--scan-backend",
            "windows-ntfs-mft-experimental",
            "--root",
            root.to_str().unwrap(),
            "--diagnostic-limit",
            "0",
        ])
        .output()
        .unwrap();

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

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Diagnostics: 1"));
    assert!(stdout.contains("Disk map diagnostics: 1 observation"));
    assert!(stdout.contains("  - fallback: 1 observation"));
    assert!(stdout.contains("  - truncated: 1 observation not shown"));
    assert!(!stdout.contains("Disk map diagnostic samples:"));
}

#[test]
fn inspect_map_ndjson_uses_v1_completed_event() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .env("REBECCA_TEST_DISABLE_LIVE_NTFS_MFT", "1")
        .args([
            "inspect",
            "map",
            "--format",
            "ndjson",
            "--scan-backend",
            "windows-ntfs-mft-experimental",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "1",
            "--group-by",
            "extension",
            "--group-limit",
            "1",
            "--diagnostic-limit",
            "0",
        ])
        .output()
        .unwrap();

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

    let events = ndjson_events(&output.stdout);
    assert!(
        events.len() >= 6,
        "expected lifecycle, progress, map rows, and completed events"
    );
    assert_eq!(events.first().unwrap()["event_kind"], "started");
    assert_eq!(events.last().unwrap()["event_kind"], "completed");
    assert_monotonic_sequences(&events);
    assert!(events.iter().all(|event| {
        event["api_version"] == "rebecca.cli.v1" && event["command"] == "inspect map"
    }));
    assert!(events.iter().any(|event| {
        event["event_kind"] == "inspect-progress"
            && event["data"]["progress_kind"] == "root-started"
    }));
    assert!(events.iter().any(|event| {
        event["event_kind"] == "inspect-progress"
            && event["data"]["progress_kind"] == "backend-fallback"
    }));
    assert!(events.iter().any(|event| {
        event["event_kind"] == "inspect-progress"
            && event["data"]["progress_kind"] == "root-finished"
    }));

    let entry = events
        .iter()
        .find(|event| event["event_kind"] == "map-entry")
        .expect("map-entry event");
    assert_eq!(entry["api_version"], "rebecca.cli.v1");
    assert_eq!(entry["event_kind"], "map-entry");
    assert_eq!(entry["command"], "inspect map");
    assert_eq!(entry["payload_kind"], "inspect-map-entry");
    assert_eq!(entry["data"]["rank"], 1);
    assert_eq!(entry["data"]["entry"]["logical_bytes"], 3);
    assert_eq!(
        entry["data"]["entry"]["estimate_backend"],
        "portable-recursive"
    );

    let group = events
        .iter()
        .find(|event| event["event_kind"] == "map-group")
        .expect("map-group event");
    assert_eq!(group["api_version"], "rebecca.cli.v1");
    assert_eq!(group["event_kind"], "map-group");
    assert_eq!(group["command"], "inspect map");
    assert_eq!(group["payload_kind"], "inspect-map-group");
    assert_eq!(group["data"]["rank"], 1);
    assert_eq!(group["data"]["group"]["kind"], "extension");
    assert_eq!(group["data"]["group"]["key"], ".bin");
    assert_eq!(group["data"]["group"]["metrics"]["logical_bytes"], 3);

    let completed = events.last().unwrap();
    assert_eq!(completed["api_version"], "rebecca.cli.v1");
    assert_eq!(completed["event_kind"], "completed");
    assert_eq!(completed["command"], "inspect map");
    assert_eq!(completed["payload_kind"], "inspect-map");
    assert_eq!(completed["data"]["totals"]["logical_bytes"], 3);
    assert_eq!(completed["data"]["diagnostic_summary"]["total"], 1);
    assert_eq!(completed["data"]["diagnostic_summary"]["retained"], 0);
    assert_eq!(completed["data"]["diagnostic_summary"]["truncated"], 1);
    assert!(
        completed["data"]["diagnostics"]
            .as_array()
            .unwrap()
            .is_empty()
    );
}

#[test]
fn inspect_map_ndjson_advice_status_filter_implies_cleanup_advice() {
    let temp = tempfile::tempdir().unwrap();
    let Some((root, target, _rule_id)) = npm_cache_rule_fixture(&temp) else {
        return;
    };
    write_fixture_file(target.join("content.bin"), b"abcdef");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "map",
            "--format",
            "ndjson",
            "--scan-backend",
            "portable-recursive",
            "--root",
            root.to_str().unwrap(),
            "--top",
            "10",
            "--entry-kind",
            "directory",
            "--path-contains",
            "_cacache",
            "--advice-status",
            "maybe-cleanable",
        ])
        .output()
        .unwrap();

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

    let events = ndjson_events(&output.stdout);
    assert_eq!(events.first().unwrap()["event_kind"], "started");
    assert_eq!(events.last().unwrap()["event_kind"], "completed");
    assert_monotonic_sequences(&events);
    assert!(
        events
            .iter()
            .any(|event| event["event_kind"] == "inspect-progress")
    );
    let entry = events
        .iter()
        .find(|event| event["event_kind"] == "map-entry")
        .expect("map-entry event");
    assert_eq!(
        PathBuf::from(entry["data"]["entry"]["path"].as_str().expect("entry path")),
        target
    );
    assert_eq!(
        entry["data"]["entry"]["cleanup_advice"]["status"],
        "maybe-cleanable"
    );

    let completed = events.last().unwrap();
    assert_eq!(completed["event_kind"], "completed");
    assert_eq!(
        completed["data"]["top_entries"].as_array().unwrap().len(),
        1
    );
    assert_eq!(
        completed["data"]["top_entries"][0]["cleanup_advice"],
        entry["data"]["entry"]["cleanup_advice"]
    );
}

#[test]
fn inspect_space_ndjson_uses_v1_completed_event() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("workspace");
    write_fixture_file(root.join("entry.bin"), b"abc");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "space",
            "--format",
            "ndjson",
            "--root",
            root.to_str().unwrap(),
        ])
        .output()
        .unwrap();

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

    let events = ndjson_events(&output.stdout);
    assert!(
        events.len() >= 4,
        "expected lifecycle, inspect progress, and completed events"
    );
    assert_eq!(events.first().unwrap()["event_kind"], "started");
    assert_eq!(events.last().unwrap()["event_kind"], "completed");
    assert_monotonic_sequences(&events);
    assert!(events.iter().all(|event| {
        event["api_version"] == "rebecca.cli.v1" && event["command"] == "inspect space"
    }));
    assert!(events.iter().any(|event| {
        event["event_kind"] == "inspect-progress"
            && event["data"]["progress_kind"] == "root-started"
    }));
    assert!(events.iter().any(|event| {
        event["event_kind"] == "inspect-progress"
            && event["data"]["progress_kind"] == "root-finished"
    }));
    let completed = events.last().unwrap();
    assert_eq!(completed["api_version"], "rebecca.cli.v1");
    assert_eq!(completed["event_kind"], "completed");
    assert_eq!(completed["command"], "inspect space");
    assert_eq!(completed["payload_kind"], "inspect-space");
    assert_eq!(completed["data"]["totals"]["estimated_bytes"], 3);
    assert_eq!(completed["data"]["diagnostic_summary"]["total"], 0);
    assert_eq!(completed["data"]["diagnostic_summary"]["retained"], 0);
    assert_eq!(completed["data"]["diagnostic_summary"]["truncated"], 0);
}

#[test]
fn inspect_artifacts_json_reports_read_only_project_artifact_insight() {
    let temp = tempfile::tempdir().unwrap();
    let workspace = temp.path().join("workspace");
    let node_modules_file = workspace.join("app").join("node_modules").join("pkg.bin");
    let target_file = workspace
        .join("app")
        .join("target")
        .join("debug")
        .join("app.bin");
    write_fixture_file(&node_modules_file, b"abc");
    write_fixture_file(&target_file, b"rust");
    write_node_project(workspace.join("app"));
    write_rust_project(workspace.join("app"));

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "artifacts",
            "--format",
            "json",
            "--no-progress",
            "--root",
            workspace.to_str().unwrap(),
            "--min-age-days",
            "0",
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    assert!(node_modules_file.exists());
    assert!(target_file.exists());
    assert!(
        !temp
            .path()
            .join("rebecca-state")
            .join("history.jsonl")
            .exists()
    );

    let envelope = common::support::api_envelope(&output.stdout);
    assert_eq!(envelope["command"], "inspect artifacts");
    assert_eq!(envelope["payload_kind"], "inspect-artifacts");

    let value = &envelope["data"];
    assert_eq!(value["summary"]["total_targets"], 2);
    assert_eq!(value["summary"]["estimated_bytes"], 7);
    assert_eq!(value["top_targets"][0]["artifact"], "target");
    assert_eq!(value["top_targets"][1]["artifact"], "node_modules");
}

#[test]
fn inspect_lint_json_reports_duplicates_and_empty_entries_without_writes() {
    let temp = tempfile::tempdir().unwrap();
    let workspace = temp.path().join("workspace");
    let reference = temp.path().join("reference");
    let protected = temp.path().join("rebecca-state");
    let duplicate = workspace.join("copy.bin");
    let reference_duplicate = reference.join("master.bin");
    let protected_duplicate = protected.join("protected.bin");
    let empty_file = workspace.join("empty.txt");
    let large_file = workspace.join("large.bin");
    let empty_dir = workspace.join("empty").join("nested");
    write_fixture_file(&duplicate, b"same");
    write_fixture_file(&reference_duplicate, b"same");
    write_fixture_file(&protected_duplicate, b"same");
    write_fixture_file(&empty_file, b"");
    write_fixture_file(&large_file, b"abcdef");
    fs::create_dir_all(&empty_dir).unwrap();

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "lint",
            "--format",
            "json",
            "--root",
            workspace.to_str().unwrap(),
            "--root",
            protected.to_str().unwrap(),
            "--reference",
            reference.to_str().unwrap(),
            "--large-file-threshold-bytes",
            "5",
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    assert!(duplicate.exists());
    assert!(reference_duplicate.exists());
    assert!(protected_duplicate.exists());
    assert!(
        !temp
            .path()
            .join("rebecca-state")
            .join("history.jsonl")
            .exists()
    );

    let envelope = common::support::api_envelope(&output.stdout);
    assert_eq!(envelope["command"], "inspect lint");
    assert_eq!(envelope["payload_kind"], "inspect-lint");

    let value = &envelope["data"];
    assert_eq!(value["summary"]["duplicate_groups"], 1);
    assert_eq!(value["summary"]["duplicate_files"], 3);
    assert_eq!(value["summary"]["conservative_reclaim_bytes"], 4);
    assert_eq!(value["summary"]["large_files"], 1);
    assert_eq!(value["summary"]["empty_files"], 1);
    assert!(value["summary"]["empty_directories"].as_u64().unwrap() >= 1);
    let group = &value["duplicate_groups"][0];
    assert_eq!(group["keep_candidates"], 2);
    let roles = group["files"]
        .as_array()
        .unwrap()
        .iter()
        .map(|file| file["role"].as_str().unwrap())
        .collect::<Vec<_>>();
    assert_eq!(roles, ["protected", "reference", "scanned"]);
    assert!(
        value["empty_directories"][0]["path"]
            .as_str()
            .unwrap()
            .ends_with("nested")
    );
}

#[test]
fn inspect_lint_ndjson_uses_v1_completed_event() {
    let temp = tempfile::tempdir().unwrap();
    let workspace = temp.path().join("workspace");
    write_fixture_file(workspace.join("a.bin"), b"same");
    write_fixture_file(workspace.join("b.bin"), b"same");

    let output = common::isolated::isolated_rebecca(&temp)
        .args([
            "inspect",
            "lint",
            "--format",
            "ndjson",
            "--root",
            workspace.to_str().unwrap(),
        ])
        .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.len(), 1);
    assert_eq!(events[0]["api_version"], "rebecca.cli.v1");
    assert_eq!(events[0]["event_kind"], "completed");
    assert_eq!(events[0]["command"], "inspect lint");
    assert_eq!(events[0]["payload_kind"], "inspect-lint");
    assert_eq!(events[0]["data"]["summary"]["duplicate_groups"], 1);
}
