//! Compatibility pack runtime (`index.pack/v1`) support.

use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use index_dom::{IndexManifest, parse_index_manifest};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use url::Url;

const COMPATIBILITY_PACK_VERSION: &str = "index.pack/v1";
const COMPATIBILITY_PACK_MAX_BYTES: usize = 64 * 1024;
const COMPATIBILITY_PACK_MAX_RULES: usize = 128;
const COMPATIBILITY_PACK_POLICY_FILE: &str = "compat-pack-runtime.conf";
const COMPATIBILITY_PACK_USER_DIR: &str = "compat-packs/user";
const COMPATIBILITY_PACK_TRUSTED_DIR: &str = "compat-packs/trusted";
const COMPATIBILITY_PACK_ROLLBACK_DIR: &str = "compat-packs/rollback";

const BUILTIN_EMPTY_PACK: &str = include_str!("../assets/compat/default.pack.json");

/// Runtime pack source tier.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackSource {
    /// User-provided local packs.
    User,
    /// Trusted local packs.
    Trusted,
    /// Built-in fallback packs bundled with Index.
    BuiltIn,
}

impl PackSource {
    /// Stable source name.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::User => "user",
            Self::Trusted => "trusted",
            Self::BuiltIn => "built-in",
        }
    }
}

/// Runtime policy loaded from CLI config.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompatibilityPackPolicy {
    /// Enables runtime pack loading.
    pub enabled: bool,
    /// Allows user pack tier.
    pub allow_user: bool,
    /// Allows trusted pack tier.
    pub allow_trusted: bool,
}

impl Default for CompatibilityPackPolicy {
    fn default() -> Self {
        Self {
            enabled: true,
            allow_user: true,
            allow_trusted: true,
        }
    }
}

#[derive(Debug, Clone)]
struct LoadedPack {
    id: String,
    source: PackSource,
    rules: Vec<LoadedRule>,
}

#[derive(Debug, Clone)]
struct LoadedRule {
    host: String,
    path_prefix: String,
    manifest: IndexManifest,
}

/// Selected manifest with attribution.
#[derive(Debug, Clone)]
pub struct SelectedPackManifest {
    /// Selected manifest.
    pub manifest: IndexManifest,
    /// Source tier that won precedence.
    pub source: PackSource,
    /// Pack identifier.
    pub pack_id: String,
    /// Host matcher used by the selected rule.
    pub rule_host: String,
    /// Path-prefix matcher used by the selected rule.
    pub rule_path_prefix: String,
}

#[derive(Debug, Deserialize)]
struct RawCompatibilityPack {
    version: String,
    id: String,
    #[serde(default)]
    rules: Vec<RawCompatibilityRule>,
}

#[derive(Debug, Deserialize)]
struct RawCompatibilityRule {
    host: String,
    #[serde(default)]
    path_prefix: Option<String>,
    #[serde(default)]
    source_url: Option<String>,
    manifest: serde_json::Value,
}

/// Loads runtime policy from `<config_dir>/compat-pack-runtime.conf`.
pub fn load_pack_policy(config_dir: &str) -> Result<CompatibilityPackPolicy, String> {
    let path = Path::new(config_dir).join(COMPATIBILITY_PACK_POLICY_FILE);
    let input = match fs::read_to_string(&path) {
        Ok(value) => value,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            return Ok(CompatibilityPackPolicy::default());
        }
        Err(error) => {
            return Err(format!(
                "failed to read compatibility pack policy {}: {error}",
                path.display()
            ));
        }
    };

    let mut policy = CompatibilityPackPolicy::default();
    for (line_number, line) in input.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let Some((raw_key, raw_value)) = trimmed.split_once('=') else {
            return Err(format!(
                "invalid compatibility pack policy line {}: expected key=value",
                line_number + 1
            ));
        };
        let key = raw_key.trim().to_ascii_lowercase();
        let value = parse_bool_config_value(raw_value.trim(), line_number + 1)?;
        match key.as_str() {
            "enabled" => policy.enabled = value,
            "allow_user" => policy.allow_user = value,
            "allow_trusted" => policy.allow_trusted = value,
            _ => {
                return Err(format!(
                    "unsupported compatibility pack policy key at line {}: {}",
                    line_number + 1,
                    raw_key.trim()
                ));
            }
        }
    }
    Ok(policy)
}

/// Lints a pack file for a target page URL.
pub fn lint_pack_file(pack_path: &str, page_url: &str) -> Result<String, String> {
    let bytes =
        fs::read(pack_path).map_err(|error| format!("failed to read {pack_path}: {error}"))?;
    let pack = parse_pack_bytes(&bytes, PackSource::User, pack_path)?;
    let normalized = normalize_url(page_url)?;
    let selected = select_rule_for_url(&pack.rules, &normalized);
    Ok(format!(
        "index-compat-pack-lint-v1\npack: {pack_path}\npage_url: {normalized}\npack_id: {}\nrules: {}\nmatch: {}\nresult: pass",
        pack.id,
        pack.rules.len(),
        selected
            .map(|rule| format!("host={} path_prefix={}", rule.host, rule.path_prefix))
            .unwrap_or_else(|| "none".to_owned())
    ))
}

/// Inspects runtime policy and selected pack for a URL.
pub fn inspect_runtime_for_url(config_dir: &str, page_url: &str) -> Result<String, String> {
    let normalized = normalize_url(page_url)?;
    let policy = load_pack_policy(config_dir)?;
    if !policy.enabled {
        return Ok(format!(
            "index-compat-pack-runtime-v1\nurl: {normalized}\npolicy: enabled=false allow_user={} allow_trusted={}\nselected: none\nresult: pass",
            policy.allow_user, policy.allow_trusted
        ));
    }

    let packs = load_runtime_packs(config_dir, policy)?;
    let selected = packs
        .iter()
        .find_map(|pack| select_rule_for_url(&pack.rules, &normalized).map(|rule| (pack, rule)));

    Ok(format!(
        "index-compat-pack-runtime-v1\nurl: {normalized}\npolicy: enabled=true allow_user={} allow_trusted={}\nloaded_packs: {}\nselected: {}\nresult: pass",
        policy.allow_user,
        policy.allow_trusted,
        packs.len(),
        selected
            .map(|(pack, rule)| format!(
                "{}:{} host={} path_prefix={}",
                pack.source.as_str(),
                pack.id,
                rule.host,
                rule.path_prefix
            ))
            .unwrap_or_else(|| "none".to_owned())
    ))
}

/// Selects a matching runtime manifest for the requested URL.
pub fn select_runtime_manifest_for_url(
    config_dir: &str,
    page_url: &str,
) -> Result<Option<SelectedPackManifest>, String> {
    let normalized = normalize_url(page_url)?;
    let policy = load_pack_policy(config_dir)?;
    if !policy.enabled {
        return Ok(None);
    }
    let packs = load_runtime_packs(config_dir, policy)?;
    for pack in &packs {
        if let Some(rule) = select_rule_for_url(&pack.rules, &normalized) {
            return Ok(Some(SelectedPackManifest {
                manifest: rule.manifest.clone(),
                source: pack.source,
                pack_id: pack.id.clone(),
                rule_host: rule.host.clone(),
                rule_path_prefix: rule.path_prefix.clone(),
            }));
        }
    }
    Ok(None)
}

fn load_runtime_packs(
    config_dir: &str,
    policy: CompatibilityPackPolicy,
) -> Result<Vec<LoadedPack>, String> {
    let mut packs = Vec::new();

    if policy.allow_user {
        let user_dir = Path::new(config_dir).join(COMPATIBILITY_PACK_USER_DIR);
        packs.extend(load_pack_dir(&user_dir, PackSource::User)?);
    }
    if policy.allow_trusted {
        let trusted_dir = Path::new(config_dir).join(COMPATIBILITY_PACK_TRUSTED_DIR);
        packs.extend(load_pack_dir(&trusted_dir, PackSource::Trusted)?);
    }
    packs.push(parse_pack_bytes(
        BUILTIN_EMPTY_PACK.as_bytes(),
        PackSource::BuiltIn,
        "built-in/default.pack.json",
    )?);

    Ok(packs)
}

fn load_pack_dir(path: &Path, source: PackSource) -> Result<Vec<LoadedPack>, String> {
    let mut entries = Vec::new();
    let directory = match fs::read_dir(path) {
        Ok(read_dir) => read_dir,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(entries),
        Err(error) => {
            return Err(format!(
                "failed to read compatibility pack directory {}: {error}",
                path.display()
            ));
        }
    };

    let mut files = Vec::new();
    for entry in directory {
        let entry = entry.map_err(|error| {
            format!(
                "failed to enumerate compatibility pack directory {}: {error}",
                path.display()
            )
        })?;
        let file_type = entry.file_type().map_err(|error| {
            format!(
                "failed to inspect compatibility pack entry {}: {error}",
                entry.path().display()
            )
        })?;
        if file_type.is_file() {
            files.push(entry.path());
        }
    }
    files.sort();

    for file in files {
        let bytes = fs::read(&file).map_err(|error| {
            format!(
                "failed to read compatibility pack {}: {error}",
                file.display()
            )
        })?;
        let origin = file.display().to_string();
        entries.push(parse_pack_bytes(&bytes, source, &origin)?);
    }

    Ok(entries)
}

fn parse_pack_bytes(bytes: &[u8], source: PackSource, origin: &str) -> Result<LoadedPack, String> {
    if bytes.len() > COMPATIBILITY_PACK_MAX_BYTES {
        return Err(format!(
            "compatibility pack {} exceeds limit: {} bytes (max {})",
            origin,
            bytes.len(),
            COMPATIBILITY_PACK_MAX_BYTES
        ));
    }
    let input = std::str::from_utf8(bytes)
        .map_err(|error| format!("compatibility pack {} is not UTF-8: {error}", origin))?;
    let raw = serde_json::from_str::<RawCompatibilityPack>(input)
        .map_err(|error| format!("compatibility pack {} is invalid JSON: {error}", origin))?;

    if raw.version != COMPATIBILITY_PACK_VERSION {
        return Err(format!(
            "compatibility pack {} uses unsupported version: {}",
            origin, raw.version
        ));
    }
    let id = validate_pack_id(&raw.id)
        .map_err(|error| format!("compatibility pack {} has invalid id: {error}", origin))?;
    if raw.rules.len() > COMPATIBILITY_PACK_MAX_RULES {
        return Err(format!(
            "compatibility pack {} has too many rules: {} (max {})",
            origin,
            raw.rules.len(),
            COMPATIBILITY_PACK_MAX_RULES
        ));
    }

    let rules = raw
        .rules
        .iter()
        .enumerate()
        .map(|(index, rule)| parse_rule(index, &id, rule))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(LoadedPack { id, source, rules })
}

fn parse_rule(
    index: usize,
    pack_id: &str,
    raw: &RawCompatibilityRule,
) -> Result<LoadedRule, String> {
    let host = validate_rule_host(&raw.host).map_err(|error| {
        format!(
            "pack {} rule {} invalid host {}: {error}",
            pack_id,
            index + 1,
            raw.host
        )
    })?;
    let path_prefix =
        validate_path_prefix(raw.path_prefix.as_deref().unwrap_or("/")).map_err(|error| {
            format!(
                "pack {} rule {} invalid path_prefix {}: {error}",
                pack_id,
                index + 1,
                raw.path_prefix.as_deref().unwrap_or("/")
            )
        })?;
    let source_url = raw
        .source_url
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
        .unwrap_or_else(|| format!("https://{host}/.well-known/index.idx"));
    let page_url = format!("https://{host}{path_prefix}");
    let manifest_json = serde_json::to_string(&raw.manifest).map_err(|error| {
        format!(
            "pack {pack_id} rule {} manifest serialization failed: {error}",
            index + 1
        )
    })?;
    let manifest =
        parse_index_manifest(&manifest_json, &source_url, &page_url).map_err(|error| {
            format!(
                "pack {} rule {} manifest is invalid: {}",
                pack_id,
                index + 1,
                error
            )
        })?;

    Ok(LoadedRule {
        host,
        path_prefix,
        manifest,
    })
}

fn validate_pack_id(value: &str) -> Result<String, String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return Err("id must not be empty".to_owned());
    }
    if trimmed.len() > 128 {
        return Err("id exceeds 128 characters".to_owned());
    }
    if !trimmed
        .chars()
        .all(|character| character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-'))
    {
        return Err("id may only use [A-Za-z0-9._-]".to_owned());
    }
    Ok(trimmed.to_owned())
}

fn validate_rule_host(value: &str) -> Result<String, String> {
    let host = value.trim().to_ascii_lowercase();
    if host.is_empty() {
        return Err("host must not be empty".to_owned());
    }
    if host.len() > 253 {
        return Err("host exceeds 253 characters".to_owned());
    }
    if !host
        .chars()
        .all(|character| character.is_ascii_alphanumeric() || matches!(character, '.' | '-'))
    {
        return Err("host may only use [A-Za-z0-9.-]".to_owned());
    }
    if !host.contains('.') {
        return Err("host must contain a dot".to_owned());
    }
    Ok(host)
}

fn validate_path_prefix(value: &str) -> Result<String, String> {
    let prefix = value.trim();
    if prefix.is_empty() {
        return Err("path_prefix must not be empty".to_owned());
    }
    if !prefix.starts_with('/') {
        return Err("path_prefix must start with '/'".to_owned());
    }
    if prefix.contains('*') || prefix.contains(' ') {
        return Err("path_prefix may not contain '*' or spaces".to_owned());
    }
    if prefix.len() > 256 {
        return Err("path_prefix exceeds 256 characters".to_owned());
    }
    Ok(prefix.to_owned())
}

fn parse_bool_config_value(value: &str, line_number: usize) -> Result<bool, String> {
    match value.trim().to_ascii_lowercase().as_str() {
        "true" => Ok(true),
        "false" => Ok(false),
        _ => Err(format!(
            "invalid compatibility pack policy boolean at line {}: {}",
            line_number, value
        )),
    }
}

fn normalize_url(input: &str) -> Result<String, String> {
    let trimmed = input.trim();
    if trimmed.contains("://") {
        return Ok(trimmed.to_owned());
    }
    Ok(format!("https://{trimmed}"))
}

fn select_rule_for_url<'a>(rules: &'a [LoadedRule], url: &str) -> Option<&'a LoadedRule> {
    let (host, path) = url_host_path(url)?;
    rules.iter().find(|rule| {
        rule.host.eq_ignore_ascii_case(host.as_str())
            && (path == rule.path_prefix
                || path
                    .strip_prefix(&rule.path_prefix)
                    .is_some_and(|rest| rest.starts_with('/') || rest.is_empty())
                || (rule.path_prefix == "/" && path.starts_with('/')))
    })
}

fn url_host_path(url: &str) -> Option<(String, String)> {
    let parsed = Url::parse(url).ok()?;
    let host = parsed.host_str()?.to_ascii_lowercase();
    let path = if parsed.path().is_empty() {
        "/".to_owned()
    } else {
        parsed.path().to_owned()
    };
    Some((host, path))
}

/// Installs a pack file into runtime user/trusted directories.
pub fn install_pack_file(
    config_dir: &str,
    pack_path: &str,
    source: PackSource,
) -> Result<PathBuf, String> {
    let bytes =
        fs::read(pack_path).map_err(|error| format!("failed to read {pack_path}: {error}"))?;
    let pack = parse_pack_bytes(&bytes, source, pack_path)?;
    let relative_dir = match source {
        PackSource::User => COMPATIBILITY_PACK_USER_DIR,
        PackSource::Trusted => COMPATIBILITY_PACK_TRUSTED_DIR,
        PackSource::BuiltIn => {
            return Err("cannot install into built-in source".to_owned());
        }
    };
    let directory = Path::new(config_dir).join(relative_dir);
    fs::create_dir_all(&directory).map_err(|error| {
        format!(
            "failed to create compatibility pack directory {}: {error}",
            directory.display()
        )
    })?;
    let target = directory.join(format!("{}.pack.json", pack.id));
    if let Ok(previous) = fs::read(&target) {
        create_rollback_snapshot(config_dir, &pack.id, source, &previous)?;
    }
    fs::write(&target, bytes).map_err(|error| {
        format!(
            "failed to write compatibility pack {}: {error}",
            target.display()
        )
    })?;
    Ok(target)
}

/// Lists local runtime pack files.
pub fn list_runtime_pack_files(config_dir: &str) -> Vec<String> {
    let mut files = Vec::new();
    for (source, relative) in [
        ("user", COMPATIBILITY_PACK_USER_DIR),
        ("trusted", COMPATIBILITY_PACK_TRUSTED_DIR),
    ] {
        let directory = Path::new(config_dir).join(relative);
        if let Ok(entries) = fs::read_dir(directory) {
            let mut paths = entries
                .flatten()
                .filter_map(|entry| {
                    entry
                        .file_type()
                        .ok()
                        .filter(|file_type| file_type.is_file())
                        .map(|_| format!("{}\t{}", source, entry.path().display()))
                })
                .collect::<Vec<_>>();
            paths.sort();
            files.extend(paths);
        }
    }
    files
}

/// Restores the most recent rollback snapshot for a pack.
pub fn rollback_pack_file(config_dir: &str, pack_id: &str) -> Result<PathBuf, String> {
    let rollback_dir = Path::new(config_dir)
        .join(COMPATIBILITY_PACK_ROLLBACK_DIR)
        .join(pack_id);
    let mut snapshots = Vec::new();
    let entries = fs::read_dir(&rollback_dir).map_err(|error| {
        format!(
            "failed to read rollback directory {}: {error}",
            rollback_dir.display()
        )
    })?;
    for entry in entries {
        let entry = entry.map_err(|error| {
            format!(
                "failed to enumerate rollback directory {}: {error}",
                rollback_dir.display()
            )
        })?;
        if entry
            .file_type()
            .map(|kind| kind.is_file())
            .unwrap_or(false)
        {
            snapshots.push(entry.path());
        }
    }
    snapshots.sort();
    let Some(snapshot) = snapshots.pop() else {
        return Err(format!("no rollback snapshots available for {pack_id}"));
    };
    let bytes = fs::read(&snapshot).map_err(|error| {
        format!(
            "failed to read rollback snapshot {}: {error}",
            snapshot.display()
        )
    })?;
    let source = if snapshot
        .file_name()
        .and_then(|name| name.to_str())
        .is_some_and(|name| name.contains(".trusted."))
    {
        PackSource::Trusted
    } else {
        PackSource::User
    };
    let target = install_bytes_as_pack(config_dir, pack_id, source, &bytes)?;
    Ok(target)
}

/// Computes a deterministic pack signature token for local trust workflows.
pub fn sign_pack_bytes(bytes: &[u8], key_id: &str, secret: &str) -> String {
    let mut hash = Sha256::new();
    hash.update(b"index.pack.signature.v1");
    hash.update([0]);
    hash.update(key_id.as_bytes());
    hash.update([0]);
    hash.update(secret.as_bytes());
    hash.update([0]);
    hash.update(bytes);
    hex_encode(hash.finalize().as_slice())
}

/// Verifies a deterministic pack signature token.
pub fn verify_pack_signature(bytes: &[u8], key_id: &str, secret: &str, signature: &str) -> bool {
    sign_pack_bytes(bytes, key_id, secret).eq_ignore_ascii_case(signature.trim())
}

fn create_rollback_snapshot(
    config_dir: &str,
    pack_id: &str,
    source: PackSource,
    bytes: &[u8],
) -> Result<(), String> {
    let directory = Path::new(config_dir)
        .join(COMPATIBILITY_PACK_ROLLBACK_DIR)
        .join(pack_id);
    fs::create_dir_all(&directory).map_err(|error| {
        format!(
            "failed to create rollback directory {}: {error}",
            directory.display()
        )
    })?;
    let stamp = unix_timestamp_nanos();
    let filename = format!("{}.{}.pack.json", stamp, source.as_str());
    let path = directory.join(filename);
    fs::write(&path, bytes).map_err(|error| {
        format!(
            "failed to write rollback snapshot {}: {error}",
            path.display()
        )
    })?;
    Ok(())
}

fn install_bytes_as_pack(
    config_dir: &str,
    pack_id: &str,
    source: PackSource,
    bytes: &[u8],
) -> Result<PathBuf, String> {
    let relative_dir = match source {
        PackSource::User => COMPATIBILITY_PACK_USER_DIR,
        PackSource::Trusted => COMPATIBILITY_PACK_TRUSTED_DIR,
        PackSource::BuiltIn => {
            return Err("cannot install into built-in source".to_owned());
        }
    };
    let directory = Path::new(config_dir).join(relative_dir);
    fs::create_dir_all(&directory).map_err(|error| {
        format!(
            "failed to create compatibility pack directory {}: {error}",
            directory.display()
        )
    })?;
    let target = directory.join(format!("{pack_id}.pack.json"));
    fs::write(&target, bytes).map_err(|error| {
        format!(
            "failed to write compatibility pack {}: {error}",
            target.display()
        )
    })?;
    Ok(target)
}

fn unix_timestamp_nanos() -> u128 {
    match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(duration) => duration.as_nanos(),
        Err(_) => 0,
    }
}

fn hex_encode(bytes: &[u8]) -> String {
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        output.push(char::from_digit(((byte >> 4) & 0x0f).into(), 16).unwrap_or('0'));
        output.push(char::from_digit((byte & 0x0f).into(), 16).unwrap_or('0'));
    }
    output
}

#[cfg(test)]
mod tests {
    use std::fs;

    use super::{
        COMPATIBILITY_PACK_POLICY_FILE, COMPATIBILITY_PACK_ROLLBACK_DIR,
        COMPATIBILITY_PACK_TRUSTED_DIR, COMPATIBILITY_PACK_USER_DIR, CompatibilityPackPolicy,
        PackSource, create_rollback_snapshot, inspect_runtime_for_url, install_bytes_as_pack,
        install_pack_file, lint_pack_file, list_runtime_pack_files, load_pack_policy,
        rollback_pack_file, select_runtime_manifest_for_url, sign_pack_bytes,
        verify_pack_signature,
    };

    fn unique_temp_dir(prefix: &str) -> std::path::PathBuf {
        let stamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        std::env::temp_dir().join(format!("index-{prefix}-{stamp}"))
    }

    fn sample_pack(host: &str, selector: &str) -> String {
        format!(
            r#"{{
  "version": "index.pack/v1",
  "id": "sample-{host}",
  "rules": [
    {{
      "host": "{host}",
      "path_prefix": "/docs",
      "manifest": {{
        "version": "index.idx/v1",
        "scope": "/docs",
        "content": {{
          "main_selector": "{selector}"
        }}
      }}
    }}
  ]
}}"#
        )
    }

    #[test]
    fn policy_defaults_when_file_missing() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-policy-default");
        let policy = load_pack_policy(dir.to_string_lossy().as_ref())?;
        assert_eq!(policy, CompatibilityPackPolicy::default());
        Ok(())
    }

    #[test]
    fn policy_parses_explicit_booleans() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-policy-config");
        fs::create_dir_all(&dir)?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_POLICY_FILE),
            "enabled=true\nallow_user=false\nallow_trusted=true\n",
        )?;
        let policy = load_pack_policy(dir.to_string_lossy().as_ref())?;
        assert_eq!(
            policy,
            CompatibilityPackPolicy {
                enabled: true,
                allow_user: false,
                allow_trusted: true
            }
        );
        Ok(())
    }

    #[test]
    fn policy_ignores_comments_and_blank_lines() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-policy-comments");
        fs::create_dir_all(&dir)?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_POLICY_FILE),
            "# comment\n\n enabled = true \n allow_user = true \n allow_trusted = false \n",
        )?;
        let policy = load_pack_policy(dir.to_string_lossy().as_ref())?;
        assert!(policy.enabled);
        assert!(policy.allow_user);
        assert!(!policy.allow_trusted);
        Ok(())
    }

    #[test]
    fn policy_rejects_invalid_lines_and_values() -> Result<(), Box<dyn std::error::Error>> {
        let missing_equals = unique_temp_dir("compat-pack-policy-missing-equals");
        fs::create_dir_all(&missing_equals)?;
        fs::write(
            missing_equals.join(COMPATIBILITY_PACK_POLICY_FILE),
            "enabled\n",
        )?;
        let error = load_pack_policy(missing_equals.to_string_lossy().as_ref())
            .err()
            .unwrap_or_default();
        assert!(error.contains("expected key=value"));

        let invalid_bool = unique_temp_dir("compat-pack-policy-invalid-bool");
        fs::create_dir_all(&invalid_bool)?;
        fs::write(
            invalid_bool.join(COMPATIBILITY_PACK_POLICY_FILE),
            "enabled=maybe\n",
        )?;
        let error = load_pack_policy(invalid_bool.to_string_lossy().as_ref())
            .err()
            .unwrap_or_default();
        assert!(error.contains("invalid compatibility pack policy boolean"));

        let invalid_key = unique_temp_dir("compat-pack-policy-invalid-key");
        fs::create_dir_all(&invalid_key)?;
        fs::write(
            invalid_key.join(COMPATIBILITY_PACK_POLICY_FILE),
            "allow_remote=true\n",
        )?;
        let error = load_pack_policy(invalid_key.to_string_lossy().as_ref())
            .err()
            .unwrap_or_default();
        assert!(error.contains("unsupported compatibility pack policy key"));
        Ok(())
    }

    #[test]
    fn lint_reports_pass_for_valid_pack() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-lint");
        fs::create_dir_all(&dir)?;
        let pack_path = dir.join("sample.pack.json");
        fs::write(&pack_path, sample_pack("example.org", "main article"))?;
        let report = lint_pack_file(
            pack_path.to_string_lossy().as_ref(),
            "https://example.org/docs/page",
        )?;
        assert!(report.contains("index-compat-pack-lint-v1"));
        assert!(report.contains("result: pass"));
        assert!(report.contains("match: host=example.org"));
        Ok(())
    }

    #[test]
    fn lint_rejects_invalid_host_rules() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-lint-invalid-host");
        fs::create_dir_all(&dir)?;
        let pack_path = dir.join("invalid.pack.json");
        fs::write(&pack_path, sample_pack("localhost", "main article"))?;
        let error = lint_pack_file(
            pack_path.to_string_lossy().as_ref(),
            "https://example.org/docs/page",
        )
        .err()
        .unwrap_or_default();
        assert!(error.contains("invalid host"));
        Ok(())
    }

    #[test]
    fn selection_prefers_user_over_trusted() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-precedence");
        fs::create_dir_all(dir.join(COMPATIBILITY_PACK_USER_DIR))?;
        fs::create_dir_all(dir.join(COMPATIBILITY_PACK_TRUSTED_DIR))?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_TRUSTED_DIR)
                .join("trusted.pack.json"),
            sample_pack("example.org", "main trusted"),
        )?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_USER_DIR).join("user.pack.json"),
            sample_pack("example.org", "main user"),
        )?;
        let selected = select_runtime_manifest_for_url(
            dir.to_string_lossy().as_ref(),
            "https://example.org/docs/page",
        )?;
        assert!(selected.is_some());
        let selected = selected.unwrap_or_else(|| unreachable!());
        assert_eq!(selected.source, PackSource::User);
        assert_eq!(selected.pack_id, "sample-example.org");
        assert_eq!(
            selected.manifest.content.main_selector.as_deref(),
            Some("main user")
        );
        Ok(())
    }

    #[test]
    fn invalid_pack_fails_closed() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-invalid");
        fs::create_dir_all(dir.join(COMPATIBILITY_PACK_USER_DIR))?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_USER_DIR)
                .join("broken.pack.json"),
            r#"{"version":"index.pack/v1","id":"broken","rules":[{"host":"example.org","manifest":{"version":"index.idx/v1","scope":"docs"}}]}"#,
        )?;
        let selected = select_runtime_manifest_for_url(
            dir.to_string_lossy().as_ref(),
            "https://example.org/docs/page",
        );
        assert!(selected.is_err());
        Ok(())
    }

    #[test]
    fn install_pack_writes_to_config_tree() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-install");
        fs::create_dir_all(&dir)?;
        let pack_path = dir.join("install.pack.json");
        fs::write(&pack_path, sample_pack("example.org", "main article"))?;
        let target = install_pack_file(
            dir.to_string_lossy().as_ref(),
            pack_path.to_string_lossy().as_ref(),
            PackSource::User,
        )?;
        assert!(target.exists());
        Ok(())
    }

    #[test]
    fn install_pack_rejects_builtin_source() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-install-builtin");
        fs::create_dir_all(&dir)?;
        let pack_path = dir.join("install.pack.json");
        fs::write(&pack_path, sample_pack("example.org", "main article"))?;
        let error = install_pack_file(
            dir.to_string_lossy().as_ref(),
            pack_path.to_string_lossy().as_ref(),
            PackSource::BuiltIn,
        )
        .err()
        .unwrap_or_default();
        assert!(error.contains("cannot install into built-in source"));
        Ok(())
    }

    #[test]
    fn install_pack_writes_to_trusted_tree() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-install-trusted");
        fs::create_dir_all(&dir)?;
        let pack_path = dir.join("install.pack.json");
        fs::write(&pack_path, sample_pack("example.org", "main article"))?;
        let target = install_pack_file(
            dir.to_string_lossy().as_ref(),
            pack_path.to_string_lossy().as_ref(),
            PackSource::Trusted,
        )?;
        assert!(target.exists());
        assert!(
            target
                .to_string_lossy()
                .contains(COMPATIBILITY_PACK_TRUSTED_DIR)
        );
        Ok(())
    }

    #[test]
    fn rollback_restores_previous_pack_snapshot() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-rollback");
        fs::create_dir_all(&dir)?;
        let first = dir.join("first.pack.json");
        let second = dir.join("second.pack.json");
        fs::write(&first, sample_pack("example.org", "main first"))?;
        fs::write(&second, sample_pack("example.org", "main second"))?;
        let _ = install_pack_file(
            dir.to_string_lossy().as_ref(),
            first.to_string_lossy().as_ref(),
            PackSource::User,
        )?;
        let installed = install_pack_file(
            dir.to_string_lossy().as_ref(),
            second.to_string_lossy().as_ref(),
            PackSource::User,
        )?;
        let before = fs::read_to_string(&installed)?;
        assert!(before.contains("main second"));
        let restored = rollback_pack_file(dir.to_string_lossy().as_ref(), "sample-example.org")?;
        let after = fs::read_to_string(&restored)?;
        assert!(after.contains("main first"));
        Ok(())
    }

    #[test]
    fn rollback_reports_missing_snapshot() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-rollback-empty");
        let rollback = dir
            .join(COMPATIBILITY_PACK_ROLLBACK_DIR)
            .join("sample-pack");
        fs::create_dir_all(&rollback)?;
        let error = rollback_pack_file(dir.to_string_lossy().as_ref(), "sample-pack")
            .err()
            .unwrap_or_default();
        assert!(error.contains("no rollback snapshots available for sample-pack"));
        Ok(())
    }

    #[test]
    fn rollback_prefers_trusted_snapshot_source() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-rollback-trusted");
        fs::create_dir_all(&dir)?;
        let bytes = sample_pack("example.org", "main trusted");
        create_rollback_snapshot(
            dir.to_string_lossy().as_ref(),
            "sample-example.org",
            PackSource::Trusted,
            bytes.as_bytes(),
        )?;
        let restored = rollback_pack_file(dir.to_string_lossy().as_ref(), "sample-example.org")?;
        assert!(
            restored
                .to_string_lossy()
                .contains(COMPATIBILITY_PACK_TRUSTED_DIR)
        );
        Ok(())
    }

    #[test]
    fn inspect_runtime_reports_disabled_policy() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-inspect-disabled");
        fs::create_dir_all(&dir)?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_POLICY_FILE),
            "enabled=false\nallow_user=true\nallow_trusted=true\n",
        )?;
        let report = inspect_runtime_for_url(dir.to_string_lossy().as_ref(), "example.org/docs")?;
        assert!(report.contains("policy: enabled=false"));
        assert!(report.contains("selected: none"));
        Ok(())
    }

    #[test]
    fn inspect_runtime_reports_selected_pack() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-inspect-selected");
        fs::create_dir_all(dir.join(COMPATIBILITY_PACK_TRUSTED_DIR))?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_TRUSTED_DIR)
                .join("trusted.pack.json"),
            sample_pack("example.org", "main trusted"),
        )?;
        let report =
            inspect_runtime_for_url(dir.to_string_lossy().as_ref(), "https://example.org/docs/a")?;
        assert!(report.contains("loaded_packs: 2"));
        assert!(report.contains("selected: trusted:sample-example.org"));
        assert!(report.contains("host=example.org path_prefix=/docs"));
        Ok(())
    }

    #[test]
    fn inspect_runtime_reports_none_when_no_rule_matches() -> Result<(), Box<dyn std::error::Error>>
    {
        let dir = unique_temp_dir("compat-pack-inspect-none");
        fs::create_dir_all(dir.join(COMPATIBILITY_PACK_USER_DIR))?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_USER_DIR).join("user.pack.json"),
            sample_pack("alpha.example", "main alpha"),
        )?;
        let report = inspect_runtime_for_url(
            dir.to_string_lossy().as_ref(),
            "https://example.org/docs/article",
        )?;
        assert!(report.contains("loaded_packs: 2"));
        assert!(report.contains("selected: none"));
        Ok(())
    }

    #[test]
    fn selection_returns_none_when_policy_disabled() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-selection-disabled");
        fs::create_dir_all(&dir)?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_POLICY_FILE),
            "enabled=false\nallow_user=true\nallow_trusted=true\n",
        )?;
        let selected = select_runtime_manifest_for_url(
            dir.to_string_lossy().as_ref(),
            "https://example.org/docs/page",
        )?;
        assert!(selected.is_none());
        Ok(())
    }

    #[test]
    fn lint_rejects_invalid_pack_shapes() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-invalid-shapes");
        fs::create_dir_all(&dir)?;

        let invalid_json = dir.join("invalid-json.pack.json");
        fs::write(&invalid_json, "{not-json}")?;
        let invalid_json_error = lint_pack_file(
            invalid_json.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(invalid_json_error.contains("invalid JSON"));

        let invalid_version = dir.join("invalid-version.pack.json");
        fs::write(
            &invalid_version,
            r#"{"version":"index.pack/v9","id":"x","rules":[]}"#,
        )?;
        let invalid_version_error = lint_pack_file(
            invalid_version.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(invalid_version_error.contains("unsupported version"));

        let invalid_id = dir.join("invalid-id.pack.json");
        fs::write(
            &invalid_id,
            r#"{"version":"index.pack/v1","id":"bad id","rules":[]}"#,
        )?;
        let invalid_id_error = lint_pack_file(
            invalid_id.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(invalid_id_error.contains("invalid id"));

        let invalid_prefix = dir.join("invalid-prefix.pack.json");
        fs::write(
            &invalid_prefix,
            r#"{
  "version":"index.pack/v1",
  "id":"invalid-prefix",
  "rules":[
    {
      "host":"example.org",
      "path_prefix":"docs",
      "manifest":{"version":"index.idx/v1","scope":"/docs"}
    }
  ]
}"#,
        )?;
        let invalid_prefix_error = lint_pack_file(
            invalid_prefix.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(invalid_prefix_error.contains("invalid path_prefix"));

        let invalid_manifest = dir.join("invalid-manifest.pack.json");
        fs::write(
            &invalid_manifest,
            r#"{
  "version":"index.pack/v1",
  "id":"invalid-manifest",
  "rules":[
    {
      "host":"example.org",
      "path_prefix":"/docs",
      "manifest":{"version":"index.idx/v1","scope":"docs"}
    }
  ]
}"#,
        )?;
        let invalid_manifest_error = lint_pack_file(
            invalid_manifest.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(invalid_manifest_error.contains("manifest is invalid"));

        Ok(())
    }

    #[test]
    fn lint_rejects_validator_limit_shapes() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-validator-limits");
        fs::create_dir_all(&dir)?;

        let empty_id = dir.join("empty-id.pack.json");
        fs::write(
            &empty_id,
            r#"{"version":"index.pack/v1","id":" ","rules":[]}"#,
        )?;
        let empty_id_error = lint_pack_file(
            empty_id.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(empty_id_error.contains("id must not be empty"));

        let long_id = dir.join("long-id.pack.json");
        let long_id_value = "a".repeat(129);
        fs::write(
            &long_id,
            format!(r#"{{"version":"index.pack/v1","id":"{long_id_value}","rules":[]}}"#),
        )?;
        let long_id_error = lint_pack_file(
            long_id.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(long_id_error.contains("id exceeds 128 characters"));

        let empty_host = dir.join("empty-host.pack.json");
        fs::write(
            &empty_host,
            r#"{
  "version":"index.pack/v1",
  "id":"empty-host",
  "rules":[
    {"host":"", "path_prefix":"/docs", "manifest":{"version":"index.idx/v1","scope":"/docs"}}
  ]
}"#,
        )?;
        let empty_host_error = lint_pack_file(
            empty_host.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(empty_host_error.contains("host must not be empty"));

        let long_host = dir.join("long-host.pack.json");
        let long_host_value = format!("{}.example.org", "a".repeat(245));
        fs::write(
            &long_host,
            format!(
                r#"{{"version":"index.pack/v1","id":"long-host","rules":[{{"host":"{long_host_value}","path_prefix":"/docs","manifest":{{"version":"index.idx/v1","scope":"/docs"}}}}]}}"#
            ),
        )?;
        let long_host_error = lint_pack_file(
            long_host.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(long_host_error.contains("host exceeds 253 characters"));

        let invalid_host_chars = dir.join("invalid-host-chars.pack.json");
        fs::write(
            &invalid_host_chars,
            r#"{
  "version":"index.pack/v1",
  "id":"invalid-host",
  "rules":[
    {"host":"exa$mple.org", "path_prefix":"/docs", "manifest":{"version":"index.idx/v1","scope":"/docs"}}
  ]
}"#,
        )?;
        let invalid_host_chars_error = lint_pack_file(
            invalid_host_chars.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(invalid_host_chars_error.contains("host may only use"));

        let empty_prefix = dir.join("empty-prefix.pack.json");
        fs::write(
            &empty_prefix,
            r#"{
  "version":"index.pack/v1",
  "id":"empty-prefix",
  "rules":[
    {"host":"example.org", "path_prefix":"", "manifest":{"version":"index.idx/v1","scope":"/docs"}}
  ]
}"#,
        )?;
        let empty_prefix_error = lint_pack_file(
            empty_prefix.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(empty_prefix_error.contains("path_prefix must not be empty"));

        let spaced_prefix = dir.join("spaced-prefix.pack.json");
        fs::write(
            &spaced_prefix,
            r#"{
  "version":"index.pack/v1",
  "id":"spaced-prefix",
  "rules":[
    {"host":"example.org", "path_prefix":"/docs section", "manifest":{"version":"index.idx/v1","scope":"/docs"}}
  ]
}"#,
        )?;
        let spaced_prefix_error = lint_pack_file(
            spaced_prefix.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(spaced_prefix_error.contains("path_prefix may not contain '*' or spaces"));

        let long_prefix = dir.join("long-prefix.pack.json");
        let long_prefix_value = format!("/{}", "a".repeat(257));
        fs::write(
            &long_prefix,
            format!(
                r#"{{"version":"index.pack/v1","id":"long-prefix","rules":[{{"host":"example.org","path_prefix":"{long_prefix_value}","manifest":{{"version":"index.idx/v1","scope":"/docs"}}}}]}}"#
            ),
        )?;
        let long_prefix_error = lint_pack_file(
            long_prefix.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(long_prefix_error.contains("path_prefix exceeds 256 characters"));

        Ok(())
    }

    #[test]
    fn list_runtime_pack_files_reports_user_and_trusted() -> Result<(), Box<dyn std::error::Error>>
    {
        let dir = unique_temp_dir("compat-pack-list");
        fs::create_dir_all(dir.join(COMPATIBILITY_PACK_USER_DIR))?;
        fs::create_dir_all(dir.join(COMPATIBILITY_PACK_TRUSTED_DIR))?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_USER_DIR)
                .join("alpha.pack.json"),
            sample_pack("alpha.example", "main alpha"),
        )?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_TRUSTED_DIR)
                .join("beta.pack.json"),
            sample_pack("beta.example", "main beta"),
        )?;
        let entries = list_runtime_pack_files(dir.to_string_lossy().as_ref());
        assert_eq!(entries.len(), 2);
        assert!(entries.iter().any(|entry| entry.starts_with("user\t")));
        assert!(entries.iter().any(|entry| entry.starts_with("trusted\t")));
        Ok(())
    }

    #[test]
    fn sign_and_verify_are_deterministic() {
        let bytes = br#"{"version":"index.pack/v1","id":"x","rules":[]}"#;
        let signature = sign_pack_bytes(bytes, "k1", "secret");
        assert!(verify_pack_signature(bytes, "k1", "secret", &signature));
        assert!(verify_pack_signature(
            bytes,
            "k1",
            "secret",
            &signature.to_ascii_uppercase()
        ));
        assert!(!verify_pack_signature(bytes, "k1", "other", &signature));
    }

    #[test]
    fn pack_source_labels_and_url_helpers_are_stable() {
        assert_eq!(PackSource::User.as_str(), "user");
        assert_eq!(PackSource::Trusted.as_str(), "trusted");
        assert_eq!(PackSource::BuiltIn.as_str(), "built-in");
        assert_eq!(
            super::normalize_url("example.org/docs"),
            Ok("https://example.org/docs".to_owned())
        );
        assert_eq!(
            super::normalize_url("https://example.org/docs"),
            Ok("https://example.org/docs".to_owned())
        );
        assert_eq!(
            super::url_host_path("https://example.org"),
            Some(("example.org".to_owned(), "/".to_owned()))
        );
    }

    #[test]
    fn load_pack_policy_reports_non_directory_config_error()
    -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-policy-read-error");
        fs::create_dir_all(&dir)?;
        let file_like_config_dir = dir.join("config-file");
        fs::write(&file_like_config_dir, "not-a-directory")?;
        let error = load_pack_policy(file_like_config_dir.to_string_lossy().as_ref())
            .err()
            .unwrap_or_default();
        assert!(error.contains("failed to read compatibility pack policy"));
        Ok(())
    }

    #[test]
    fn lint_rejects_oversized_non_utf8_and_excessive_rule_counts()
    -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-parse-guards");
        fs::create_dir_all(&dir)?;

        let oversized = dir.join("oversized.pack.json");
        fs::write(
            &oversized,
            vec![b'x'; super::COMPATIBILITY_PACK_MAX_BYTES + 1],
        )?;
        let oversized_error = lint_pack_file(
            oversized.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(oversized_error.contains("exceeds limit"));

        let non_utf8 = dir.join("non-utf8.pack.json");
        fs::write(&non_utf8, [0xff_u8, 0xfe_u8, 0xfd_u8])?;
        let non_utf8_error = lint_pack_file(
            non_utf8.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(non_utf8_error.contains("is not UTF-8"));

        let rules = (0..(super::COMPATIBILITY_PACK_MAX_RULES + 1))
            .map(|index| {
                format!(
                    r#"{{"host":"r{index}.example.org","path_prefix":"/docs","manifest":{{"version":"index.idx/v1","scope":"/docs"}}}}"#
                )
            })
            .collect::<Vec<_>>()
            .join(",");
        let too_many = dir.join("too-many-rules.pack.json");
        fs::write(
            &too_many,
            format!(r#"{{"version":"index.pack/v1","id":"too-many","rules":[{rules}]}}"#),
        )?;
        let too_many_error = lint_pack_file(
            too_many.to_string_lossy().as_ref(),
            "https://example.org/docs",
        )
        .err()
        .unwrap_or_default();
        assert!(too_many_error.contains("too many rules"));
        Ok(())
    }

    #[test]
    fn install_pack_reports_directory_creation_errors() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-install-dir-error");
        fs::create_dir_all(&dir)?;
        let blocked_config_dir = dir.join("blocked-config-root");
        fs::write(&blocked_config_dir, "not-a-directory")?;
        let pack_path = dir.join("valid.pack.json");
        fs::write(&pack_path, sample_pack("example.org", "main article"))?;
        let error = install_pack_file(
            blocked_config_dir.to_string_lossy().as_ref(),
            pack_path.to_string_lossy().as_ref(),
            PackSource::User,
        )
        .err()
        .unwrap_or_default();
        assert!(error.contains("failed to create compatibility pack directory"));
        Ok(())
    }

    #[test]
    fn rollback_reports_missing_directory_error() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-rollback-missing-dir");
        fs::create_dir_all(&dir)?;
        let error = rollback_pack_file(dir.to_string_lossy().as_ref(), "missing-pack")
            .err()
            .unwrap_or_default();
        assert!(error.contains("failed to read rollback directory"));
        Ok(())
    }

    #[test]
    fn inspect_runtime_reports_directory_scan_errors() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-inspect-scan-error");
        fs::create_dir_all(dir.join("compat-packs"))?;
        fs::write(dir.join(COMPATIBILITY_PACK_USER_DIR), "not-a-directory")?;
        let user_error =
            inspect_runtime_for_url(dir.to_string_lossy().as_ref(), "https://example.org/docs")
                .err()
                .unwrap_or_default();
        assert!(user_error.contains("failed to read compatibility pack directory"));

        fs::remove_file(dir.join(COMPATIBILITY_PACK_USER_DIR))?;
        fs::write(
            dir.join(COMPATIBILITY_PACK_POLICY_FILE),
            "enabled=true\nallow_user=false\nallow_trusted=true\n",
        )?;
        fs::write(dir.join(COMPATIBILITY_PACK_TRUSTED_DIR), "not-a-directory")?;
        let trusted_error =
            inspect_runtime_for_url(dir.to_string_lossy().as_ref(), "https://example.org/docs")
                .err()
                .unwrap_or_default();
        assert!(trusted_error.contains("failed to read compatibility pack directory"));
        Ok(())
    }

    #[test]
    fn install_bytes_rejects_builtin_target() {
        let error = install_bytes_as_pack(
            "/tmp/index-nonexistent",
            "sample-pack",
            PackSource::BuiltIn,
            br#"{"version":"index.pack/v1","id":"sample-pack","rules":[]}"#,
        )
        .err()
        .unwrap_or_default();
        assert!(error.contains("cannot install into built-in source"));
    }

    #[test]
    fn install_bytes_writes_to_trusted_tree() -> Result<(), Box<dyn std::error::Error>> {
        let dir = unique_temp_dir("compat-pack-install-bytes-trusted");
        fs::create_dir_all(&dir)?;
        let target = install_bytes_as_pack(
            dir.to_string_lossy().as_ref(),
            "sample-trusted-pack",
            PackSource::Trusted,
            br#"{"version":"index.pack/v1","id":"sample-trusted-pack","rules":[]}"#,
        )?;
        assert!(target.exists());
        assert!(
            target
                .to_string_lossy()
                .contains(COMPATIBILITY_PACK_TRUSTED_DIR)
        );
        Ok(())
    }
}
