//! End-to-end integration tests for the init -> mcp add -> sync flow.
//!
//! These tests exercise the public API only, using temporary files so no
//! real user configuration is touched.
//!
//! # Dimensions covered
//!
//! - MCP sync (original tests)
//! - Hooks sync (F3)
//! - Skills sync (F3)
//! - Rules sync (F3)
//! - Combined multi-dimension sync (F3)

use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs;
use std::path::Path;

use lorum::config::{
    HookHandler, HooksConfig, LorumConfig, McpConfig, McpServer, ProjectConfig, load_config,
    load_project_config, merge_configs, save_config,
};
use lorum::env_interpolate;
use lorum::sync;

// ---------------------------------------------------------------------------
// T7.1 – init -> add servers -> verify file
// ---------------------------------------------------------------------------

#[test]
fn init_creates_config_and_add_servers() {
    let dir = tempfile::tempdir().unwrap();
    let config_path = dir.path().join("config.yaml");

    // "init" – save a default config (mirrors commands::run_init).
    let cfg = LorumConfig::default();
    save_config(&config_path, &cfg).unwrap();
    assert!(config_path.exists());

    // "mcp add" – insert servers and save.
    let mut cfg = load_config(&config_path).unwrap();
    cfg.mcp.servers.insert(
        "fetch".into(),
        McpServer {
            command: "npx".into(),
            args: vec!["-y".into(), "@modelcontextprotocol/server-fetch".into()],
            env: BTreeMap::new(),
        },
    );
    cfg.mcp.servers.insert(
        "memory".into(),
        McpServer {
            command: "npx".into(),
            args: vec!["-y".into(), "@modelcontextprotocol/server-memory".into()],
            env: BTreeMap::new(),
        },
    );
    save_config(&config_path, &cfg).unwrap();

    // Verify the file on disk round-trips correctly.
    let loaded = load_config(&config_path).unwrap();
    assert_eq!(loaded.mcp.servers.len(), 2);
    assert!(loaded.mcp.servers.contains_key("fetch"));
    assert!(loaded.mcp.servers.contains_key("memory"));
    assert_eq!(
        loaded.mcp.servers["fetch"].args,
        vec!["-y", "@modelcontextprotocol/server-fetch"]
    );

    // Verify the raw YAML contains the expected keys.
    let raw = fs::read_to_string(&config_path).unwrap();
    assert!(raw.contains("fetch"));
    assert!(raw.contains("memory"));
    assert!(raw.contains("npx"));
}

// ---------------------------------------------------------------------------
// T7.1 – sync_all produces results for every registered adapter
// ---------------------------------------------------------------------------

#[test]
fn sync_produces_results_for_all_adapters() {
    let mcp = McpConfig {
        servers: {
            let mut m = BTreeMap::new();
            m.insert(
                "test-server".into(),
                McpServer {
                    command: "echo".into(),
                    args: vec!["hello".into()],
                    env: BTreeMap::new(),
                },
            );
            m
        },
    };

    let results = sync::sync_all(&mcp);
    // We have 9 registered adapters.
    assert_eq!(results.len(), 9);

    let names: Vec<&str> = results.iter().map(|r| r.tool.as_str()).collect();
    assert!(names.contains(&"claude-code"));
    assert!(names.contains(&"codex"));
    assert!(names.contains(&"continue"));
    assert!(names.contains(&"cursor"));
    assert!(names.contains(&"proma"));
    assert!(names.contains(&"kimi"));
    assert!(names.contains(&"opencode"));
    assert!(names.contains(&"trae"));
    assert!(names.contains(&"windsurf"));

    for result in &results {
        // Each result should report the correct number of servers synced on
        // success, or have an error message on failure.
        if result.success {
            assert_eq!(result.servers_synced, 1);
            assert!(result.error.is_none());
        } else {
            assert!(result.error.is_some());
        }
    }
}

// ---------------------------------------------------------------------------
// T7.1 – full roundtrip: add servers -> compute diff -> verify
// ---------------------------------------------------------------------------

#[test]
fn full_roundtrip_add_sync_verify() {
    let dir = tempfile::tempdir().unwrap();
    let config_path = dir.path().join("config.yaml");

    // Step 1: create empty config.
    save_config(&config_path, &LorumConfig::default()).unwrap();

    // Step 2: add three servers.
    let mut cfg = load_config(&config_path).unwrap();
    for (name, cmd) in [("alpha", "a"), ("beta", "b"), ("gamma", "c")] {
        cfg.mcp.servers.insert(
            name.into(),
            McpServer {
                command: cmd.into(),
                args: vec![],
                env: BTreeMap::new(),
            },
        );
    }
    save_config(&config_path, &cfg).unwrap();

    // Step 3: compute diff – current is empty, target has 3 servers.
    let current = McpConfig::default();
    let target = cfg.mcp.clone();
    let diff = sync::compute_diff(&current, &target);

    assert_eq!(diff.added, vec!["alpha", "beta", "gamma"]);
    assert!(diff.removed.is_empty());
    assert!(diff.modified.is_empty());
    assert!(diff.unchanged.is_empty());
    assert!(!diff.is_empty());
    assert_eq!(diff.change_count(), 3);

    // Step 4: compute diff again – now both sides match.
    let diff_same = sync::compute_diff(&target, &target);
    assert!(diff_same.added.is_empty());
    assert!(diff_same.removed.is_empty());
    assert!(diff_same.modified.is_empty());
    assert_eq!(diff_same.unchanged.len(), 3);
    assert!(diff_same.is_empty()); // no *changes*
}

// ---------------------------------------------------------------------------
// T7.3 – backup and restore roundtrip
// ---------------------------------------------------------------------------

#[test]
#[serial_test::serial]
fn backup_and_restore_roundtrip() {
    let dir = tempfile::tempdir().unwrap();
    let config_path = dir.path().join("config.yaml");

    let xdg = dir.path().join("xdg_config");
    let _guard = EnvGuard::set("XDG_CONFIG_HOME", &xdg);

    // Create an original config.
    let original = LorumConfig {
        mcp: McpConfig {
            servers: {
                let mut m = BTreeMap::new();
                m.insert(
                    "original-server".into(),
                    McpServer {
                        command: "original-cmd".into(),
                        args: vec!["arg1".into()],
                        env: BTreeMap::new(),
                    },
                );
                m
            },
        },
        ..Default::default()
    };
    save_config(&config_path, &original).unwrap();

    // Create a backup.
    let backup_path = lorum::backup::create_backup("test-restore", &config_path).unwrap();
    assert!(backup_path.exists());

    // Modify the config.
    let modified = LorumConfig {
        mcp: McpConfig {
            servers: {
                let mut m = BTreeMap::new();
                m.insert(
                    "modified-server".into(),
                    McpServer {
                        command: "modified-cmd".into(),
                        args: vec![],
                        env: BTreeMap::new(),
                    },
                );
                m
            },
        },
        ..Default::default()
    };
    save_config(&config_path, &modified).unwrap();

    // Verify it was modified.
    let loaded = load_config(&config_path).unwrap();
    assert!(loaded.mcp.servers.contains_key("modified-server"));
    assert!(!loaded.mcp.servers.contains_key("original-server"));

    // Restore from backup.
    lorum::backup::restore_backup("test-restore", &config_path).unwrap();

    // Verify the restored content matches the original.
    let restored = load_config(&config_path).unwrap();
    assert_eq!(restored, original);
}

// ---------------------------------------------------------------------------
// T7.3 – backup cleanup keeps max 10
// ---------------------------------------------------------------------------

#[test]
#[serial_test::serial]
fn backup_cleanup_keeps_max() {
    let dir = tempfile::tempdir().unwrap();
    let config_path = dir.path().join("config.yaml");

    let xdg = dir.path().join("xdg_config");
    let _guard = EnvGuard::set("XDG_CONFIG_HOME", &xdg);

    // Write a dummy file to back up.
    fs::write(&config_path, "test content").unwrap();

    // Create 12 backups. After each one, old backups should be pruned to 10.
    for _ in 0..12 {
        // Small sleep to get unique timestamps.
        std::thread::sleep(std::time::Duration::from_millis(5));
        lorum::backup::create_backup("test-cleanup", &config_path).unwrap();
    }

    let backups = lorum::backup::list_backups("test-cleanup").unwrap();
    assert!(
        backups.len() <= 10,
        "expected at most 10 backups, got {}",
        backups.len()
    );
}

// ---------------------------------------------------------------------------
// T7.4 – global + project merge
// ---------------------------------------------------------------------------

#[test]
fn global_plus_project_merge() {
    let global = LorumConfig {
        mcp: McpConfig {
            servers: {
                let mut m = BTreeMap::new();
                m.insert(
                    "global-srv".into(),
                    McpServer {
                        command: "g".into(),
                        args: vec![],
                        env: BTreeMap::new(),
                    },
                );
                m.insert(
                    "shared-srv".into(),
                    McpServer {
                        command: "global-version".into(),
                        args: vec![],
                        env: BTreeMap::new(),
                    },
                );
                m
            },
        },
        ..Default::default()
    };

    let project = ProjectConfig {
        mcp: McpConfig {
            servers: {
                let mut m = BTreeMap::new();
                m.insert(
                    "project-srv".into(),
                    McpServer {
                        command: "p".into(),
                        args: vec![],
                        env: BTreeMap::new(),
                    },
                );
                m.insert(
                    "shared-srv".into(),
                    McpServer {
                        command: "project-version".into(),
                        args: vec![],
                        env: BTreeMap::new(),
                    },
                );
                m
            },
        },
        exclude: vec![],
        ..Default::default()
    };

    let merged = merge_configs(&global, Some(&project));

    // global-srv is preserved, project-srv is added, shared-srv is overridden.
    assert_eq!(merged.mcp.servers.len(), 3);
    assert_eq!(merged.mcp.servers["global-srv"].command, "g");
    assert_eq!(merged.mcp.servers["project-srv"].command, "p");
    assert_eq!(merged.mcp.servers["shared-srv"].command, "project-version");
}

// ---------------------------------------------------------------------------
// T7.4 – exclude removes servers
// ---------------------------------------------------------------------------

#[test]
fn exclude_removes_servers() {
    let global = LorumConfig {
        mcp: McpConfig {
            servers: {
                let mut m = BTreeMap::new();
                for name in &["keep", "remove-me", "also-keep"] {
                    m.insert(
                        (*name).into(),
                        McpServer {
                            command: "cmd".into(),
                            args: vec![],
                            env: BTreeMap::new(),
                        },
                    );
                }
                m
            },
        },
        ..Default::default()
    };

    let project = ProjectConfig {
        mcp: McpConfig::default(),
        exclude: vec!["remove-me".into()],
        ..Default::default()
    };

    let merged = merge_configs(&global, Some(&project));
    assert_eq!(merged.mcp.servers.len(), 2);
    assert!(merged.mcp.servers.contains_key("keep"));
    assert!(merged.mcp.servers.contains_key("also-keep"));
    assert!(!merged.mcp.servers.contains_key("remove-me"));
}

// ---------------------------------------------------------------------------
// T7.4 – merge with no project config returns global as-is
// ---------------------------------------------------------------------------

#[test]
fn merge_no_project_returns_global() {
    let global = LorumConfig {
        mcp: McpConfig {
            servers: {
                let mut m = BTreeMap::new();
                m.insert(
                    "only-global".into(),
                    McpServer {
                        command: "cmd".into(),
                        args: vec![],
                        env: BTreeMap::new(),
                    },
                );
                m
            },
        },
        ..Default::default()
    };

    let merged = merge_configs(&global, None);
    assert_eq!(merged, global);
}

// ---------------------------------------------------------------------------
// T7.4 – project config file roundtrip via save/load
// ---------------------------------------------------------------------------

#[test]
fn project_config_save_and_load() {
    let dir = tempfile::tempdir().unwrap();
    let proj_dir = dir.path().join(".lorum");
    fs::create_dir_all(&proj_dir).unwrap();
    let config_path = proj_dir.join("config.yaml");

    // Write a project config with exclude list.
    let yaml = r#"
mcp:
  servers:
    local-srv:
      command: ./run.sh
      args: []
      env: {}
exclude:
  - remote-heavy
"#;
    fs::write(&config_path, yaml).unwrap();

    let loaded = load_project_config(&config_path)
        .unwrap()
        .expect("should load project config");
    assert_eq!(loaded.mcp.servers.len(), 1);
    assert_eq!(loaded.mcp.servers["local-srv"].command, "./run.sh");
    assert_eq!(loaded.exclude, vec!["remote-heavy"]);
}

// ---------------------------------------------------------------------------
// T7.1 – config roundtrip: save then load preserves content
// ---------------------------------------------------------------------------

#[test]
fn config_roundtrip_load_save() {
    let dir = tempfile::tempdir().unwrap();
    let config_path = dir.path().join("config.yaml");

    let original = LorumConfig {
        mcp: McpConfig {
            servers: {
                let mut m = BTreeMap::new();
                m.insert(
                    "alpha".into(),
                    McpServer {
                        command: "echo".into(),
                        args: vec!["alpha-arg".into()],
                        env: {
                            let mut e = BTreeMap::new();
                            e.insert("ALPHA_ENV".into(), "alpha-value".into());
                            e
                        },
                    },
                );
                m.insert(
                    "beta".into(),
                    McpServer {
                        command: "run".into(),
                        args: vec![],
                        env: BTreeMap::new(),
                    },
                );
                m
            },
        },
        ..Default::default()
    };

    // Save, then load, then verify content matches.
    save_config(&config_path, &original).unwrap();
    let loaded = load_config(&config_path).unwrap();
    assert_eq!(loaded, original);
}

// ---------------------------------------------------------------------------
// T7.1 – compute_diff detects added and removed servers
// ---------------------------------------------------------------------------

#[test]
fn compute_diff_detects_added_removed() {
    // Current config has server-a and server-b.
    let current = McpConfig {
        servers: {
            let mut m = BTreeMap::new();
            m.insert(
                "server-a".into(),
                McpServer {
                    command: "a".into(),
                    args: vec![],
                    env: BTreeMap::new(),
                },
            );
            m.insert(
                "server-b".into(),
                McpServer {
                    command: "b".into(),
                    args: vec![],
                    env: BTreeMap::new(),
                },
            );
            m
        },
    };

    // Target config has server-b and server-c.
    let target = McpConfig {
        servers: {
            let mut m = BTreeMap::new();
            m.insert(
                "server-b".into(),
                McpServer {
                    command: "b".into(),
                    args: vec![],
                    env: BTreeMap::new(),
                },
            );
            m.insert(
                "server-c".into(),
                McpServer {
                    command: "c".into(),
                    args: vec![],
                    env: BTreeMap::new(),
                },
            );
            m
        },
    };

    let diff = sync::compute_diff(&current, &target);

    // server-a was removed, server-c was added, server-b is unchanged.
    assert!(diff.added.contains(&"server-c".into()));
    assert!(diff.removed.contains(&"server-a".into()));
    assert!(diff.unchanged.contains(&"server-b".into()));
    assert!(diff.modified.is_empty());
    assert_eq!(diff.change_count(), 2);
}

// ---------------------------------------------------------------------------
// T7.1 – env interpolation preserves unset vars and respects expand flag
// ---------------------------------------------------------------------------

#[test]
fn env_interpolation_expands_vars() {
    // When expand=false, the original string is returned unchanged.
    let no_expand = env_interpolate::interpolate_env("${LORUM_MISSING_VAR}", false);
    assert_eq!(no_expand, "${LORUM_MISSING_VAR}");

    // When expand=true but the variable is not set, ${VAR} is preserved as-is.
    let unset = env_interpolate::interpolate_env("${LORUM_UNSET_VAR_12345}", true);
    assert_eq!(unset, "${LORUM_UNSET_VAR_12345}");

    // Plain text without variables passes through unchanged.
    let plain = env_interpolate::interpolate_env("plain text here", true);
    assert_eq!(plain, "plain text here");

    // Multiple placeholders in one string are all preserved when unset.
    let multi = env_interpolate::interpolate_env("${A} and ${B} and ${C}", true);
    assert_eq!(multi, "${A} and ${B} and ${C}");
}

// ---------------------------------------------------------------------------
// F3 – Hooks sync integration tests
// ---------------------------------------------------------------------------

/// Helper: build a sample `HooksConfig` with two events.
fn sample_hooks_config() -> HooksConfig {
    let mut config = HooksConfig::default();
    config.events.insert(
        "pre-tool-use".into(),
        vec![HookHandler {
            matcher: "Bash".into(),
            command: "scripts/safety-check.sh".into(),
            timeout: Some(60),
            handler_type: None,
        }],
    );
    config.events.insert(
        "post-tool-use".into(),
        vec![HookHandler {
            matcher: "Write|Edit".into(),
            command: "cargo fmt".into(),
            timeout: None,
            handler_type: None,
        }],
    );
    config
}

#[test]
fn sync_hooks_all_produces_results_for_all_adapters() {
    let hooks = sample_hooks_config();
    let results = sync::sync_hooks_all(&hooks);
    // 6 registered hooks adapters.
    assert_eq!(results.len(), 6);

    let names: Vec<&str> = results.iter().map(|r| r.tool.as_str()).collect();
    assert!(names.contains(&"claude-code"));
    assert!(names.contains(&"kimi"));
    assert!(names.contains(&"cursor"));
    assert!(names.contains(&"codex"));
    assert!(names.contains(&"windsurf"));
    assert!(names.contains(&"opencode"));
}

#[test]
fn sync_hooks_tools_filters_to_specified_tools() {
    let hooks = sample_hooks_config();
    let results = sync::sync_hooks_tools(&hooks, &["opencode".into(), "codex".into()]);
    assert_eq!(results.len(), 2);
    let names: Vec<&str> = results.iter().map(|r| r.tool.as_str()).collect();
    assert!(names.contains(&"opencode"));
    assert!(names.contains(&"codex"));
}

#[test]
fn sync_hooks_tools_reports_unknown_adapter() {
    let hooks = sample_hooks_config();
    let results = sync::sync_hooks_tools(&hooks, &["nonexistent-tool".into()]);
    assert_eq!(results.len(), 1);
    assert!(!results[0].success);
    assert!(results[0].error.as_ref().unwrap().contains("not found"));
}

#[test]
fn dry_run_hooks_all_reads_without_writing() {
    let dir = tempfile::tempdir().unwrap();
    let original_home = std::env::var_os("HOME");

    let result = std::panic::catch_unwind(|| {
        unsafe { std::env::set_var("HOME", dir.path()) };

        let hooks = sample_hooks_config();
        let results = sync::dry_run_hooks_all(&hooks);
        assert_eq!(results.len(), 6);
        for r in &results {
            assert!(
                r.success,
                "dry_run read failed for {}: {:?}",
                r.tool, r.error
            );
        }

        // Verify no hooks config files were created in temp home.
        assert!(!dir.path().join(".claude").join("settings.json").exists());
        assert!(!dir.path().join(".kimi").join("config.toml").exists());
    });

    unsafe {
        match original_home {
            Some(v) => std::env::set_var("HOME", v),
            None => std::env::remove_var("HOME"),
        }
    }
    assert!(result.is_ok());
}

// ---------------------------------------------------------------------------
// F3 – Skills sync integration tests
// ---------------------------------------------------------------------------

/// Helper: create a skill directory with SKILL.md under `parent`.
fn make_skill_dir(parent: &Path, name: &str) -> std::path::PathBuf {
    let dir = parent.join(name);
    fs::create_dir_all(&dir).unwrap();
    fs::write(
        dir.join("SKILL.md"),
        format!("---\nname: {name}\ndescription: \"Test skill\"\n---\n# {name}\n"),
    )
    .unwrap();
    dir
}

#[test]
fn sync_skills_all_syncs_to_all_adapters() {
    let src = tempfile::tempdir().unwrap();
    make_skill_dir(src.path(), "integration-test-skill");

    let results = sync::sync_skills_all(src.path());
    // 8 registered skills adapters.
    assert_eq!(results.len(), 8);

    let names: Vec<&str> = results.iter().map(|r| r.tool.as_str()).collect();
    assert!(names.contains(&"claude-code"));
    assert!(names.contains(&"codex"));
    assert!(names.contains(&"cursor"));
    assert!(names.contains(&"kimi"));
    assert!(names.contains(&"opencode"));
    assert!(names.contains(&"proma"));
    assert!(names.contains(&"trae"));
    assert!(names.contains(&"windsurf"));
}

#[test]
fn sync_skills_tools_filters_correctly() {
    let src = tempfile::tempdir().unwrap();
    make_skill_dir(src.path(), "filtered-skill");

    let results = sync::sync_skills_tools(src.path(), &["codex".into(), "kimi".into()]);
    assert_eq!(results.len(), 2);
    let names: Vec<&str> = results.iter().map(|r| r.tool.as_str()).collect();
    assert!(names.contains(&"codex"));
    assert!(names.contains(&"kimi"));
}

#[test]
fn sync_skills_tools_unknown_adapter_fails() {
    let src = tempfile::tempdir().unwrap();
    let results = sync::sync_skills_tools(src.path(), &["no-such-tool".into()]);
    assert_eq!(results.len(), 1);
    assert!(!results[0].success);
    assert!(results[0].error.as_ref().unwrap().contains("not found"));
}

#[test]
fn dry_run_skills_all_does_not_write() {
    let home = tempfile::tempdir().unwrap();
    let src = tempfile::tempdir().unwrap();
    make_skill_dir(src.path(), "dry-skill");

    let original_home = std::env::var_os("HOME");
    let result = std::panic::catch_unwind(|| {
        unsafe { std::env::set_var("HOME", home.path()) };

        let results = sync::dry_run_skills_all(src.path());
        assert_eq!(results.len(), 8);
        for r in &results {
            assert!(r.success, "dry_run failed for {}: {:?}", r.tool, r.error);
        }

        // No skills directories should have been created under temp home.
        assert!(!home.path().join(".claude").join("skills").exists());
        assert!(!home.path().join(".codex").join("skills").exists());
    });

    unsafe {
        match original_home {
            Some(v) => std::env::set_var("HOME", v),
            None => std::env::remove_var("HOME"),
        }
    }
    assert!(result.is_ok());
}

// ---------------------------------------------------------------------------
// F3 – Rules sync integration tests
// ---------------------------------------------------------------------------

#[test]
#[serial_test::serial]
fn sync_rules_all_syncs_to_all_adapters() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();

    let xdg = dir.path().join("xdg_config");
    let _guard = EnvGuard::set("XDG_CONFIG_HOME", &xdg);

    let content = "# Project Rules\n\n## Style\nUse 4 spaces.\n";
    let results = sync::sync_rules_all(root, content);
    assert_eq!(results.len(), 7);
    for r in &results {
        assert!(r.success, "rules sync failed for {}: {:?}", r.tool, r.error);
    }

    // Verify a few well-known files.
    assert_eq!(
        fs::read_to_string(root.join(".cursorrules")).unwrap(),
        content
    );
    assert_eq!(
        fs::read_to_string(root.join(".windsurfrules")).unwrap(),
        content
    );
    assert_eq!(
        fs::read_to_string(root.join(".codex").join("rules.md")).unwrap(),
        content
    );
}

#[test]
fn sync_rules_tools_filters_correctly() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();

    let content = "# Rules\n";
    let results = sync::sync_rules_tools(root, content, &["cursor".into(), "codex".into()]);
    assert_eq!(results.len(), 2);
    assert!(results[0].success);
    assert!(results[1].success);

    let names: Vec<&str> = results.iter().map(|r| r.tool.as_str()).collect();
    assert!(names.contains(&"cursor"));
    assert!(names.contains(&"codex"));
}

#[test]
fn sync_rules_tools_unknown_adapter_fails() {
    let dir = tempfile::tempdir().unwrap();
    let results = sync::sync_rules_tools(dir.path(), "# Rules\n", &["ghost-tool".into()]);
    assert_eq!(results.len(), 1);
    assert!(!results[0].success);
    assert!(results[0].error.as_ref().unwrap().contains("not found"));
}

#[test]
fn dry_run_rules_all_does_not_write() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();

    let content = "# Rules\n## Testing\nRun cargo test\n";
    let results = sync::dry_run_rules_all(root, content);
    assert_eq!(results.len(), 7);
    for r in &results {
        assert!(
            r.success,
            "dry_run read failed for {}: {:?}",
            r.tool, r.error
        );
        assert!(r.needs_update, "{} should need update", r.tool);
    }

    // No files should have been written.
    assert!(!root.join(".cursorrules").exists());
    assert!(!root.join(".windsurfrules").exists());
    assert!(!root.join(".codex").join("rules.md").exists());
}

// ---------------------------------------------------------------------------
// F3 – Combined multi-dimension sync (simulates --hooks --skills --rules)
// ---------------------------------------------------------------------------

#[test]
#[serial_test::serial]
fn combined_sync_hooks_skills_rules_succeeds() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    let original_home = std::env::var_os("HOME");

    let xdg = dir.path().join("xdg_config");
    let _guard = EnvGuard::set("XDG_CONFIG_HOME", &xdg);

    let result = std::panic::catch_unwind(|| {
        unsafe { std::env::set_var("HOME", dir.path()) };

        // 1. Hooks sync
        let hooks = sample_hooks_config();
        let hooks_results = sync::sync_hooks_all(&hooks);
        assert_eq!(hooks_results.len(), 6);

        // 2. Skills sync
        let skills_src = dir.path().join("unified-skills");
        fs::create_dir_all(&skills_src).unwrap();
        make_skill_dir(&skills_src, "combined-skill");
        let skills_results = sync::sync_skills_all(&skills_src);
        assert_eq!(skills_results.len(), 8);

        // 3. Rules sync
        let rules_content = "# Combined Rules\n## Style\nUse tabs.\n";
        let rules_results = sync::sync_rules_all(root, rules_content);
        assert_eq!(rules_results.len(), 7);
        for r in &rules_results {
            assert!(r.success, "rules sync failed for {}: {:?}", r.tool, r.error);
        }

        // Verify rules files exist.
        assert!(root.join(".cursorrules").exists());
        assert_eq!(
            fs::read_to_string(root.join(".cursorrules")).unwrap(),
            rules_content
        );
    });

    unsafe {
        match original_home {
            Some(v) => std::env::set_var("HOME", v),
            None => std::env::remove_var("HOME"),
        }
    }
    assert!(result.is_ok());
}

#[test]
fn combined_dry_run_hooks_skills_rules_does_not_mutate() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    let original_home = std::env::var_os("HOME");

    let result = std::panic::catch_unwind(|| {
        unsafe { std::env::set_var("HOME", dir.path()) };

        // Hooks dry run
        let hooks = sample_hooks_config();
        let hooks_dry = sync::dry_run_hooks_all(&hooks);
        assert_eq!(hooks_dry.len(), 6);
        for r in &hooks_dry {
            assert!(r.success);
        }

        // Skills dry run
        let skills_src = dir.path().join("unified-skills-dry");
        fs::create_dir_all(&skills_src).unwrap();
        make_skill_dir(&skills_src, "dry-skill");
        let skills_dry = sync::dry_run_skills_all(&skills_src);
        assert_eq!(skills_dry.len(), 8);
        for r in &skills_dry {
            assert!(r.success);
        }

        // Rules dry run
        let rules_dry = sync::dry_run_rules_all(root, "# Rules\n");
        assert_eq!(rules_dry.len(), 7);
        for r in &rules_dry {
            assert!(r.success);
        }

        // Nothing was written.
        assert!(!root.join(".cursorrules").exists());
        assert!(!root.join(".windsurfrules").exists());
    });

    unsafe {
        match original_home {
            Some(v) => std::env::set_var("HOME", v),
            None => std::env::remove_var("HOME"),
        }
    }
    assert!(result.is_ok());
}

// ---------------------------------------------------------------------------
// EnvGuard helper for tests that need to override XDG_CONFIG_HOME
// ---------------------------------------------------------------------------

struct EnvGuard {
    key: &'static str,
    old: Option<OsString>,
}

impl EnvGuard {
    fn set(key: &'static str, value: &Path) -> Self {
        let old = std::env::var_os(key);
        unsafe {
            std::env::set_var(key, value);
        }
        Self { key, old }
    }
}

impl Drop for EnvGuard {
    fn drop(&mut self) {
        match &self.old {
            Some(v) => unsafe { std::env::set_var(self.key, v) },
            None => unsafe { std::env::remove_var(self.key) },
        }
    }
}
