//! Blocked-surface rules. Distinguishes **runtime exhaust** (always blocked)
//! from **repo-tracked agent material** (allowed, classified as
//! `ProjectAgentFile`).
//!
//! ADR 0013 — the original Cordance spec blocked `.claude` wholesale, which
//! falsely flagged axiom's own repo-tracked `.claude/settings.json`. This
//! splits the rule by path *under* the agent dir.
//!
//! Path-substring rules apply to any segment of the repo-relative path.
//! Filename rules match only on the final path component, so paths like
//! `crates/.environment/foo.rs` are not falsely flagged as `.env` files.

use cordance_core::paths::{
    segment_matches_any_ascii_case_insensitive, segments_match_ascii_case_insensitive,
};

/// Single-component directory names that block when present as ANY full path
/// segment. A path matches if `any-segment == name`.
///
/// Round-8 redteam #3: the previous `PATH_SUBSTRINGS` list used naive
/// `p.contains(".git/")`, which false-blocked `myrepo.git/objects/pack.idx`
/// — bare-repo mirror layouts use `<name>.git/` as the working dir, NOT
/// `.git/`. Segment-exact matching catches the literal `.git` directory
/// while leaving `myrepo.git` alone. Same fix applies to every name in
/// this list — none of them should false-block on a longer parent name.
///
/// `.cordance` (round-5 bughunt #1 CRITICAL): Cordance's own state
/// directory must never enter `pack.sources` or every file's hash would
/// be embedded in `pack.json`, creating a self-reference loop where
/// consecutive `cordance pack` runs produce byte-different output.
///
/// `.git` (round-6 redteam #3 / bughunt #1 CRITICAL): the git database
/// has state that mutates on every git operation (`index`, `ORIG_HEAD`,
/// `logs/`, `gc.log`, `packed-refs`, `objects/`) — these would otherwise
/// break run-to-run determinism AND leak operator PII (`.git/config`
/// carries committer email + remote URLs).
const BLOCKED_SEGMENT_NAMES: &[&str] = &[
    ".cordance",
    ".git",
    ".codex-logs",
    "node_modules",
    "target",
    "dist",
    "build",
    "coverage",
    ".pytest_cache",
    "__pycache__",
    ".idea",
    ".vscode",
];

/// Multi-component directory subpaths that block when present as a
/// contiguous segment run inside the path. A path matches if any window of
/// `subpath.len()` consecutive segments equals the subpath.
///
/// These differentiate between repo-tracked agent material and runtime
/// exhaust under the same parent. `.claude/cache/` is exhaust;
/// `.claude/settings.json` is repo-tracked. Same shape for `.codex/`.
const BLOCKED_SUBPATHS: &[&[&str]] = &[
    &[".claude", "cache"],
    &[".claude", "sessions"],
    &[".claude", "worktrees"],
    &[".claude", "projects"],
    &[".codex", "cache"],
    &[".codex", "sessions"],
];

/// Filenames that always indicate secrets or credential material when they
/// appear as the final path component.
///
/// Round-4 bughunt HIGH R4-bughunt-8: these are compared against the
/// ASCII-lowercased final component, so default-case-insensitive NTFS / APFS
/// variants like `Credentials.json` or `SECRETS.JSON` block too. Every entry
/// here MUST be lowercase — the comparison key is `final_component_lower`.
const SECRET_FILENAMES: &[&str] = &[
    "id_rsa",
    "id_ed25519",
    "id_ecdsa",
    "id_dsa",
    "secrets.json",
    "secrets.yaml",
    "secrets.yml",
    "credentials.json",
    "credentials.yaml",
    ".npmrc",
    ".pypirc",
    ".netrc",
    "gcp-key.json",
];

/// Filenames that are OS / editor exhaust, not credential material.
///
/// Round-4 bughunt HIGH R4-bughunt-8: stored lowercase to match the
/// case-folded comparison key used by `is_blocked` / `block_reason`. On disk
/// these files appear as `.DS_Store` and `Thumbs.db`, but the scanner
/// compares the lowercased final component so case variants are caught.
const OS_JUNK_FILENAMES: &[&str] = &[".ds_store", "thumbs.db"];

/// File extensions that always indicate runtime exhaust when present on the
/// final path component. Compared case-insensitively (`FOO.LOG` blocks too).
const BLOCKED_EXTENSIONS: &[&str] = &["log", "pem", "key", "sqlite", "db", "profraw"];

/// True if the final path component has an extension matching any entry in
/// `exts` (ASCII-case-insensitive). Uses `std::path::Path::extension()` so
/// composite names like `foo.dbg.bin` are not mis-matched against `db`.
fn has_blocked_extension(final_component: &str, exts: &[&str]) -> bool {
    std::path::Path::new(final_component)
        .extension()
        .and_then(std::ffi::OsStr::to_str)
        .is_some_and(|ext| exts.iter().any(|target| ext.eq_ignore_ascii_case(target)))
}

/// True if the path is runtime exhaust that must never be imported.
///
/// Decision shape (in order):
/// 1. Path-substring rule — matches anywhere in the (forward-slash-normalised)
///    path. Catches runtime exhaust directories like `target/`, `.codex-logs/`.
/// 2. Filename-component rule — matches only on the final path component.
///    Catches `.env`, `.env.*`, `id_rsa`, `secrets.*`, and other secret
///    surfaces without false-positives on names like `.environment` or
///    `dotenv-loader.rs`.
/// 3. Filename-suffix rule — extension-style match on the final component.
///
/// ## Case sensitivity (Round-4 bughunt HIGH R4-bughunt-8)
///
/// The final component is ASCII-lowercased once at the top of the function;
/// every subsequent `starts_with` / `ends_with` / `contains` / equality check
/// uses the lowercased form. Default-case-insensitive filesystems (NTFS,
/// APFS) treat `SECRET-FOO.TXT` and `secret-foo.txt` as the same file, so
/// the scanner blocks both. The static `SECRET_FILENAMES` and
/// `OS_JUNK_FILENAMES` tables store lowercase keys to match.
#[must_use]
pub fn is_blocked(rel_path: &str) -> bool {
    let p = rel_path.replace('\\', "/");

    if path_matches_block_rules(&p) {
        return true;
    }

    let final_component = p.rsplit('/').next().unwrap_or(p.as_str());
    let final_component_lower = final_component.to_ascii_lowercase();

    // Round-7 redteam #2 / bughunt #1 CRITICAL: a bare `.git` FILE at the
    // repo root is a git-worktree pointer (`gitdir: /abs/path/to/main/repo/
    // .git/worktrees/xxx`). The round-6 `.git/` substring rule only catches
    // path segments with a trailing slash — `.git` (no slash) at the root
    // slipped through and leaked the operator's absolute filesystem path
    // into pack.sources. Block the bare component too.
    if final_component_lower == ".git" {
        return true;
    }

    if final_component_lower == ".env" || final_component_lower.starts_with(".env.") {
        return true;
    }
    if SECRET_FILENAMES.contains(&final_component_lower.as_str())
        || OS_JUNK_FILENAMES.contains(&final_component_lower.as_str())
    {
        return true;
    }
    if final_component_lower.starts_with("secret") || final_component_lower.ends_with("_secret") {
        return true;
    }
    // `.secret` as a name component is also blocked — catches the
    // `MY.SECRET.txt` shape used by some IDEs to stash key material with a
    // capitalised middle segment. Both mid-name (`.secret.`) and trailing
    // (`.secret`) forms are caught.
    if final_component_lower.contains(".secret.") || final_component_lower.ends_with(".secret") {
        return true;
    }
    if has_blocked_extension(&final_component_lower, BLOCKED_EXTENSIONS) {
        return true;
    }

    false
}

/// Segment-aware match against `BLOCKED_SEGMENT_NAMES` and `BLOCKED_SUBPATHS`.
///
/// Splits the (forward-slash-normalised) path into segments and:
/// 1. Returns `true` if any segment equals a name in `BLOCKED_SEGMENT_NAMES`.
///    Catches `.git/`, `.cordance/`, `target/`, etc. as full segments and
///    avoids false-blocking longer parent names (`myrepo.git/`, `mytarget/`).
/// 2. Returns `true` if any window of consecutive segments equals a subpath
///    in `BLOCKED_SUBPATHS`. Catches `.claude/cache/`, `.codex/sessions/`
///    while leaving `.claude/settings.json` alone.
fn path_matches_block_rules(forward_slashed: &str) -> bool {
    let segments: Vec<&str> = forward_slashed
        .split('/')
        .filter(|s| !s.is_empty())
        .collect();

    if segments
        .iter()
        .any(|seg| segment_matches_any_ascii_case_insensitive(seg, BLOCKED_SEGMENT_NAMES))
    {
        return true;
    }

    for subpath in BLOCKED_SUBPATHS {
        if segments.len() >= subpath.len()
            && segments
                .windows(subpath.len())
                .any(|w| segments_match_ascii_case_insensitive(w, subpath))
        {
            return true;
        }
    }

    false
}

/// Block-reason classifier — mirrors `is_blocked`'s segment-aware logic but
/// returns a human-readable category. Used by `cordance scan` to render the
/// blocked-surface section of the audit report.
fn block_reason_from_segments(forward_slashed: &str) -> Option<&'static str> {
    let segments: Vec<&str> = forward_slashed
        .split('/')
        .filter(|s| !s.is_empty())
        .collect();

    // Single-segment matches first — most informative reason strings.
    for seg in &segments {
        if seg.eq_ignore_ascii_case(".cordance") {
            return Some("cordance internal state");
        }
        if seg.eq_ignore_ascii_case(".git") {
            return Some("git internal state");
        }
        if seg.eq_ignore_ascii_case(".codex-logs") {
            return Some("codex runtime exhaust");
        }
        if segment_matches_any_ascii_case_insensitive(
            seg,
            &[
                "node_modules",
                "target",
                "dist",
                "build",
                "coverage",
                ".pytest_cache",
                "__pycache__",
            ],
        ) {
            return Some("build / vendor artifact");
        }
        if segment_matches_any_ascii_case_insensitive(seg, &[".idea", ".vscode"]) {
            return Some("ide state");
        }
    }

    // Two-segment subpaths.
    for window in segments.windows(2) {
        if window[0].eq_ignore_ascii_case(".claude")
            && segment_matches_any_ascii_case_insensitive(
                window[1],
                &["cache", "sessions", "worktrees", "projects"],
            )
        {
            return Some("claude runtime exhaust");
        }
        if window[0].eq_ignore_ascii_case(".codex")
            && segment_matches_any_ascii_case_insensitive(window[1], &["cache", "sessions"])
        {
            return Some("codex runtime exhaust");
        }
    }

    None
}

/// Human-readable reason for blocking a path.
///
/// Mirrors `is_blocked` but produces a category label rather than a boolean.
/// Returns `None` for paths that wouldn't be blocked.
#[must_use]
pub fn block_reason(rel_path: &str) -> Option<&'static str> {
    let p = rel_path.replace('\\', "/");

    if let Some(reason) = block_reason_from_segments(&p) {
        return Some(reason);
    }

    let final_component = p.rsplit('/').next().unwrap_or(p.as_str());
    // Round-4 bughunt HIGH R4-bughunt-8: lowercase once for the entire
    // filename-component decision block. Mirrors `is_blocked` so the two
    // functions never disagree about which paths block.
    let final_component_lower = final_component.to_ascii_lowercase();

    // Round-7 redteam #2 / bughunt #1: bare `.git` file (worktree pointer).
    if final_component_lower == ".git" {
        return Some("git internal state");
    }

    if final_component_lower == ".env" || final_component_lower.starts_with(".env.") {
        return Some("environment / secrets file");
    }
    if SECRET_FILENAMES.contains(&final_component_lower.as_str()) {
        return Some("credential material");
    }
    if OS_JUNK_FILENAMES.contains(&final_component_lower.as_str()) {
        return Some("os junk");
    }
    if final_component_lower.starts_with("secret") || final_component_lower.ends_with("_secret") {
        return Some("credential material");
    }
    if final_component_lower.contains(".secret.") || final_component_lower.ends_with(".secret") {
        return Some("credential material");
    }
    if has_blocked_extension(&final_component_lower, &["pem", "key"]) {
        return Some("credential material");
    }
    if has_blocked_extension(&final_component_lower, &["sqlite", "db"]) {
        return Some("binary state");
    }
    if has_blocked_extension(&final_component_lower, &["log"]) {
        return Some("log file");
    }
    if has_blocked_extension(&final_component_lower, &["profraw"]) {
        return Some("coverage exhaust");
    }

    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn runtime_session_blocked() {
        assert!(is_blocked(".claude/sessions/foo.json"));
        assert!(is_blocked(".codex-logs/foo.log"));
    }

    /// Round-6 redteam #3 / bughunt #1 CRITICAL: every file under `.git/`
    /// must be blocked. `.git/index` mutates on every git operation, so
    /// two consecutive `cordance pack` runs against the same commit would
    /// otherwise produce byte-different `pack.json`. Also `.git/config`
    /// embeds committer email and remote URLs — PII the operator shouldn't
    /// ship through MCP wire responses.
    #[test]
    fn git_internal_state_blocked() {
        assert!(is_blocked(".git/index"));
        assert!(is_blocked(".git/config"));
        assert!(is_blocked(".git/HEAD"));
        assert!(is_blocked(".git/logs/HEAD"));
        assert!(is_blocked(".git/packed-refs"));
        assert!(is_blocked(".git/objects/pack/pack-abc.pack"));
        assert!(is_blocked(".git/gc.log"));
        assert_eq!(block_reason(".git/index"), Some("git internal state"));
    }

    /// `.gitignore` and `.gitattributes` live at the repo root, NOT inside
    /// `.git/`. They are repo-tracked configuration and must NOT block.
    #[test]
    fn gitignore_at_root_not_blocked() {
        assert!(!is_blocked(".gitignore"));
        assert!(!is_blocked(".gitattributes"));
        assert!(!is_blocked("subproject/.gitignore"));
    }

    /// Round-7 redteam #2 / bughunt #1 CRITICAL: a BARE `.git` file (not a
    /// directory with trailing slash) is a git worktree pointer; its
    /// contents are `gitdir: /abs/path/to/main/.git/worktrees/xxx`. The
    /// round-6 `.git/` substring rule misses it because the substring needs
    /// the slash. The bare-component rule catches it.
    #[test]
    fn bare_git_pointer_file_blocked() {
        assert!(is_blocked(".git"));
        assert!(is_blocked("subproject/.git"));
        assert_eq!(block_reason(".git"), Some("git internal state"));
    }

    /// Case-insensitive match for `.git` — should not over-block files like
    /// `gitlab-ci.yml` or `.github/workflows/ci.yml`.
    #[test]
    fn dot_git_is_exact_not_prefix() {
        assert!(!is_blocked(".github"));
        assert!(!is_blocked(".github/workflows/ci.yml"));
        assert!(!is_blocked("gitlab-ci.yml"));
        assert!(!is_blocked("dotgit-helper.sh"));
    }

    /// Round-8 redteam #3: bare-repo mirror layouts use `<name>.git/` as the
    /// working dir, NOT `.git/`. The previous `path.contains(".git/")`
    /// substring rule false-blocked every file under any directory whose
    /// name ended in `.git`. Segment-exact matching catches the literal
    /// `.git` directory but leaves `myrepo.git/`, `bare.git/`, etc. alone.
    #[test]
    fn bare_repo_mirror_directories_not_blocked() {
        assert!(!is_blocked("myrepo.git/objects/pack.idx"));
        assert!(!is_blocked("bare.git/config"));
        assert!(!is_blocked("x.git/README.md"));
        assert!(!is_blocked("nested/path/foo.git/HEAD"));
        // The legitimate `.git` directory inside still blocks.
        assert!(is_blocked("myrepo.git/.git/index"));
    }

    /// Round-8 carryover: `target/release/foo` must block, but `mytarget/foo`
    /// must not. The substring rule used to match both because `mytarget/`
    /// contains `target/` as a substring. Segment-exact match is the fix.
    #[test]
    fn segment_exact_blocking_does_not_over_block() {
        assert!(is_blocked("target/release/cordance"));
        assert!(!is_blocked("mytarget/foo.rs"));
        assert!(!is_blocked("docs/target-tracking.md"));
        assert!(is_blocked("node_modules/pkg/index.js"));
        assert!(!is_blocked("mynode_modules/foo.js"));
        assert!(!is_blocked("docs/node_modules-policy.md"));
    }

    /// R9 path-policy blocker: on NTFS-like case-insensitive filesystems,
    /// case variants of blocked directory segments address the same surfaces
    /// as their canonical lowercase spellings. Segment matching must remain
    /// exact about boundaries while ignoring ASCII case.
    #[test]
    fn blocked_segments_are_ascii_case_insensitive() {
        assert!(is_blocked(".GIT/index"));
        assert_eq!(block_reason(".GIT/index"), Some("git internal state"));

        assert!(is_blocked(".Cordance/pack.json"));
        assert_eq!(
            block_reason(".Cordance/pack.json"),
            Some("cordance internal state")
        );

        assert!(is_blocked("Target/release/foo"));
        assert_eq!(
            block_reason("Target/release/foo"),
            Some("build / vendor artifact")
        );

        assert!(!is_blocked("myTarget/foo.rs"));
        assert!(!is_blocked("docs/Target-tracking.md"));
        assert!(!is_blocked(".Cordance-cache/pack.json"));
    }

    /// Round-5 bughunt CRITICAL R5-bughunt-1: every file under `.cordance/`
    /// must be blocked so it never enters `pack.sources`. Otherwise
    /// `pack.json` would embed the hash of the previous run's `pack.json`,
    /// and consecutive `cordance pack` runs against an unchanged source tree
    /// would produce byte-different outputs.
    #[test]
    fn cordance_internal_state_blocked() {
        assert!(is_blocked(".cordance/pack.json"));
        assert!(is_blocked(".cordance/sources.lock"));
        assert!(is_blocked(".cordance/evidence-map.json"));
        assert!(is_blocked(".cordance/cortex-receipt.json"));
        assert!(is_blocked(".cordance/llm-candidate.json"));
        assert!(is_blocked(".cordance/scan-report.md"));
        assert!(is_blocked(".cordance/cache/doctrine/abc/HEAD"));
        assert_eq!(
            block_reason(".cordance/pack.json"),
            Some("cordance internal state")
        );
    }

    /// Documenting that paths that merely *contain* `.cordance` as a
    /// substring elsewhere (e.g. `.cordance-cache/` as a sibling directory
    /// outside the project root, used by `paths::doctrine_cache_root`'s
    /// fallback) do NOT get accidentally blocked by the rule.
    #[test]
    fn unrelated_cordance_prefix_not_blocked() {
        // `.cordance-cache/` is the home-dir fallback location, but it's
        // outside the project tree so a repo-relative path never contains it.
        assert!(!is_blocked("docs/cordance-design.md"));
        assert!(!is_blocked("src/cordance_helpers.rs"));
    }

    #[test]
    fn repo_tracked_agent_file_not_blocked() {
        assert!(!is_blocked(".claude/settings.json"));
        assert!(!is_blocked("AGENTS.md"));
        assert!(!is_blocked("agents/codex/AGENTS.md"));
    }

    #[test]
    fn dotenv_subdirectory_not_blocked() {
        assert!(!is_blocked("crates/.environment/foo.rs"));
        assert!(!is_blocked("tests/dotenv-loader.rs"));
        assert!(!is_blocked("docs/dotenv-guide.md"));
    }

    #[test]
    fn dotenv_file_blocked() {
        assert!(is_blocked(".env"));
        assert!(is_blocked("apps/.env"));
        assert!(is_blocked(".env.local"));
        assert!(is_blocked(".env.production"));
        assert!(is_blocked(".env.staging"));
        assert!(is_blocked("apps/web/.env.local"));
    }

    #[test]
    fn id_rsa_blocked() {
        assert!(is_blocked("id_rsa"));
        assert!(is_blocked("~/.ssh/id_rsa"));
        assert!(is_blocked(".ssh/id_ed25519"));
        assert!(is_blocked("path/to/id_ecdsa"));
        assert!(is_blocked("path/to/id_dsa"));
    }

    #[test]
    fn secrets_and_credentials_blocked() {
        assert!(is_blocked("secrets.json"));
        assert!(is_blocked("config/secrets.yaml"));
        assert!(is_blocked("config/secrets.yml"));
        assert!(is_blocked("credentials.json"));
        assert!(is_blocked("credentials.yaml"));
        assert!(is_blocked(".npmrc"));
        assert!(is_blocked(".pypirc"));
        assert!(is_blocked(".netrc"));
        assert!(is_blocked("gcp-key.json"));
    }

    #[test]
    fn secret_prefix_and_suffix_blocked() {
        // Filename-component prefix rule: a *file* named `secret*` is blocked.
        assert!(is_blocked("secret-token.txt"));
        assert!(is_blocked("dir/secret-token.txt"));
        // Filename-component suffix rule: a file ending `_secret` is blocked.
        assert!(is_blocked("path/to/api_secret"));
        assert!(is_blocked("api_secret"));
        // A directory called `secrets/` does not, by itself, block its
        // children — they must still match a filename rule. This mirrors how
        // engineering doctrine treats containment vs. content classification.
        assert!(!is_blocked("secrets/notes.md"));
    }

    #[test]
    fn db_suffix_only_matches_extension() {
        assert!(is_blocked("foo.db"));
        assert!(!is_blocked("report.dbghelp.txt"));
        assert!(!is_blocked("library.dbg.bin"));
    }

    #[test]
    fn log_suffix_blocked() {
        assert!(is_blocked("server.log"));
        assert!(is_blocked("logs/app.log"));
        assert!(!is_blocked("logs.md"));
    }

    #[test]
    fn block_reason_matches_is_blocked() {
        // For any blocked path, block_reason should return Some.
        let blocked_samples = [
            ".env",
            ".env.staging",
            "id_rsa",
            "secrets.json",
            "foo.db",
            "server.log",
            ".claude/sessions/foo.json",
            "node_modules/pkg/index.js",
            "target/release/cordance",
        ];
        for s in blocked_samples {
            assert!(is_blocked(s), "expected blocked: {s}");
            assert!(block_reason(s).is_some(), "expected reason for: {s}");
        }
    }

    #[test]
    fn block_reason_none_for_clean_paths() {
        assert!(block_reason("src/lib.rs").is_none());
        assert!(block_reason("docs/adr/0001.md").is_none());
        assert!(block_reason("crates/.environment/foo.rs").is_none());
    }

    /// Round-4 bughunt HIGH R4-bughunt-8: on default-case-insensitive NTFS
    /// and APFS, `SECRET-FOO.txt` and `secret-foo.txt` are the same file.
    /// The prefix check must be ASCII-case-insensitive so both block.
    #[test]
    fn upper_case_secret_prefix_blocked() {
        assert!(is_blocked("SECRET-FOO.txt"));
        assert!(is_blocked("SECRET-token.txt"));
        assert!(is_blocked("Secret-Token.txt"));
        assert!(is_blocked("dir/SECRET-FOO.txt"));
        assert!(block_reason("SECRET-FOO.txt").is_some());
    }

    /// Round-4 bughunt HIGH R4-bughunt-8: capitalised credential filenames
    /// must match the static `SECRET_FILENAMES` table case-insensitively.
    #[test]
    fn capitalised_credentials_blocked() {
        assert!(is_blocked("Credentials.json"));
        assert!(is_blocked("CREDENTIALS.JSON"));
        assert!(is_blocked("Secrets.json"));
        assert!(is_blocked("SECRETS.YAML"));
        assert!(is_blocked("config/Credentials.json"));
        assert!(is_blocked("GCP-KEY.JSON"));
        assert!(block_reason("Credentials.json") == Some("credential material"));
        assert!(block_reason("CREDENTIALS.JSON") == Some("credential material"));
    }

    /// Round-4 bughunt HIGH R4-bughunt-8: caps in the *middle* of a name
    /// must also trigger blocking when the lowercased form contains a
    /// `.secret.` substring or ends in `.secret`.
    #[test]
    fn mid_name_caps_secret_blocked() {
        assert!(is_blocked("MY.SECRET.txt"));
        assert!(is_blocked("my.secret.txt"));
        assert!(is_blocked("MY.SECRET")); // ends with `.secret`
        assert!(is_blocked("foo.SECRET.json"));
        assert!(is_blocked("dir/foo.SECRET.json"));
    }

    /// Round-4 bughunt HIGH R4-bughunt-8: OS junk filenames are case-folded
    /// too. Both the canonical `.DS_Store` and any odd case variants like
    /// `.ds_store` block.
    #[test]
    fn os_junk_case_insensitive() {
        assert!(is_blocked(".DS_Store"));
        assert!(is_blocked(".ds_store"));
        assert!(is_blocked(".DS_STORE"));
        assert!(is_blocked("subdir/.DS_Store"));
        assert!(is_blocked("Thumbs.db"));
        assert!(is_blocked("thumbs.DB"));
        assert!(is_blocked("THUMBS.DB"));
        assert!(block_reason(".DS_Store") == Some("os junk"));
    }
}
