use std::path::PathBuf;

use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;

#[derive(Parser)]
#[command(
    name = "astmap",
    version,
    about = "Code structure index with transitive impact analysis"
)]
struct Cli {
    /// Project root directory
    #[arg(short = 'p', long, default_value = ".")]
    project_dir: PathBuf,

    /// Log level (error, warn, info, debug, trace)
    #[arg(short = 'l', long, default_value = "warn")]
    log_level: String,

    /// Output as JSON
    #[arg(long)]
    json: bool,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Scan the project and build/update the structural index
    Scan {
        /// Force full re-scan (ignore content hashes)
        #[arg(long)]
        full: bool,
    },
    /// Search symbols by name or keyword
    Query {
        /// Search pattern
        pattern: String,
        /// Filter by symbol kind
        #[arg(long)]
        kind: Option<String>,
        /// Maximum results
        #[arg(long, default_value = "20")]
        limit: i64,
    },
    /// Get detailed info for a specific symbol
    Inspect {
        /// Symbol name or ID
        symbol: String,
    },
    /// Transitive impact analysis
    Impact {
        /// Symbol name or ID
        symbol: String,
        /// Maximum traversal depth
        #[arg(long, default_value = "3")]
        depth: i64,
    },
    /// Find dependency path between two symbols
    Why {
        /// Source symbol
        from: String,
        /// Target symbol
        to: String,
        /// Maximum search depth
        #[arg(long, default_value = "5")]
        depth: i64,
    },
    /// Map git diff to affected symbols
    Diff {
        /// Base branch or commit
        #[arg(long)]
        base: Option<String>,
        /// Impact analysis depth
        #[arg(long, default_value = "3")]
        depth: i64,
    },
    /// Export structural index to Markdown
    Export {
        /// Output file (default: stdout)
        #[arg(short, long)]
        output: Option<PathBuf>,
        /// Approximate token limit
        #[arg(long)]
        token_budget: Option<i64>,
    },
    /// Show structural changes since a git reference (added/removed/modified symbols)
    Since {
        /// Git reference (SHA, branch, tag, HEAD~N)
        reference: String,
        /// Impact analysis depth
        #[arg(long, default_value = "3")]
        depth: i64,
    },
    /// Structural digest since last session (auto-detects reference)
    Digest {
        /// Impact analysis depth
        #[arg(long, default_value = "3")]
        depth: i64,
    },
    /// Manage configuration
    Config {
        #[command(subcommand)]
        action: ConfigAction,
    },
    /// Start MCP Server (stdio transport for LLM tool integration)
    Mcp,
    /// Watch for file changes and auto-update the index
    Watch,
    /// Background daemon for automatic index updates with socket interface
    Daemon {
        #[command(subcommand)]
        action: DaemonAction,
    },
    /// Subscribe to daemon structural change events
    Subscribe {
        /// Glob filter for trigger files (e.g. "src/**/*.rs")
        #[arg(long)]
        filter: Option<String>,
        /// Output raw JSON (default for pipe, overrides --pretty)
        #[arg(long)]
        json: bool,
        /// Pretty-print output (default for TTY)
        #[arg(long)]
        pretty: bool,
    },
}

#[derive(Subcommand)]
enum DaemonAction {
    /// Start the daemon (foreground, Ctrl+C to stop)
    Start,
    /// Stop the running daemon
    Stop,
    /// Check daemon status
    Status,
}

#[derive(Subcommand)]
enum ConfigAction {
    /// Get a config value
    Get { key: String },
    /// Set a config value
    Set { key: String, value: String },
    /// List all config entries
    List,
}

pub async fn run() -> Result<()> {
    let cli = Cli::parse();

    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&cli.log_level)),
        )
        .with_writer(std::io::stderr)
        .init();

    run_command(cli).await
}

async fn run_command(cli: Cli) -> Result<()> {
    let project_dir = std::fs::canonicalize(&cli.project_dir)?;

    match cli.command {
        Commands::Scan { full } => {
            let result = astmap::ops::scan(&project_dir, full)?;
            if cli.json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "status": "completed",
                        "files": {
                            "total": result.files_total,
                            "skipped": result.files_skipped,
                            "partial": result.files_partial,
                        },
                        "symbols": result.symbols,
                        "dependencies": result.dependencies,
                    }))?
                );
            } else {
                println!("Scan complete:");
                println!(
                    "  Files:        {} ({} skipped, {} partial)",
                    result.files_total, result.files_skipped, result.files_partial
                );
                println!("  Symbols:      {}", result.symbols);
                println!("  Dependencies: {}", result.dependencies);
            }
        }
        Commands::Query {
            pattern,
            kind: _,
            limit,
        } => {
            let results = astmap::ops::query(&project_dir, &pattern, limit)?;
            if cli.json {
                println!("{}", serde_json::to_string_pretty(&results)?);
            } else {
                println!("Found {} results for \"{}\":\n", results.len(), pattern);
                for r in &results {
                    println!(
                        "  {} {}  {}:{}-{}",
                        r.kind, r.name, r.file_path, r.start_line, r.end_line
                    );
                    if let Some(sig) = &r.signature {
                        println!("    {}", sig);
                    }
                }
            }
        }
        Commands::Inspect { symbol } => {
            let result = astmap::ops::inspect(&project_dir, &symbol)?;
            let s = &result.symbol;
            if cli.json {
                println!("{}", serde_json::to_string_pretty(&result)?);
            } else {
                println!("{} {} (id: {})", s.kind, s.name, s.id);
                if let Some(sig) = &s.signature {
                    println!("  Signature: {}", sig);
                }
                println!("  Lines: {}-{}", s.start_line, s.end_line);
                if !result.children.is_empty() {
                    println!("  Children:");
                    for c in &result.children {
                        println!("    {} {} (line {})", c.kind, c.name, c.start_line);
                    }
                }
            }
        }
        Commands::Impact { symbol, depth } => {
            let result = astmap::ops::impact(&project_dir, &symbol, depth)?;
            if cli.json {
                println!("{}", serde_json::to_string_pretty(&result.entries)?);
            } else {
                println!("Impact analysis for {} (depth: {}):\n", symbol, depth);
                let mut current_depth = 0;
                for entry in &result.entries {
                    if entry.depth != current_depth {
                        current_depth = entry.depth;
                        println!(
                            "Level {} ({}):",
                            current_depth,
                            if current_depth == 1 {
                                "direct"
                            } else {
                                "indirect"
                            }
                        );
                    }
                    println!(
                        "  <- {} {}  {}",
                        entry.symbol_kind, entry.symbol_name, entry.file_path
                    );
                }
                if result.entries.is_empty() {
                    println!("  No dependents found.");
                } else {
                    println!("\nTotal: {} symbols affected", result.entries.len());
                }
            }
        }
        Commands::Why { from, to, depth } => {
            let result = astmap::ops::why_path(&project_dir, &from, &to, depth)?;
            if cli.json {
                println!("{}", serde_json::to_string_pretty(&result.path)?);
            } else if result.path.is_empty() {
                println!("No dependency path found between {} and {}", from, to);
            } else {
                println!("Dependency path from {} to {}:\n", from, to);
                for (i, step) in result.path.iter().enumerate() {
                    let connector = if i < result.path.len() - 1 {
                        format!(" --[{}]-->", step.dep_type)
                    } else {
                        String::new()
                    };
                    println!("  {} ({}){}", step.symbol_name, step.file_path, connector);
                }
            }
        }
        Commands::Diff { base, depth } => {
            let result = astmap::ops::diff(&project_dir, base.as_deref(), depth)?;
            if cli.json {
                println!("{}", serde_json::to_string_pretty(&result)?);
            } else {
                if result.changed_files.is_empty() && result.unindexed_files.is_empty() {
                    println!("No changes detected.");
                } else {
                    println!("Changed files:\n");
                    for cf in &result.changed_files {
                        println!("  {} [{}]", cf.path, cf.status);
                        for sym in &cf.affected_symbols {
                            println!(
                                "    {} {} (line {}-{})",
                                sym.kind, sym.name, sym.start_line, sym.end_line
                            );
                        }
                    }
                    if !result.unindexed_files.is_empty() {
                        println!("\nUnindexed files (run scan first):");
                        for f in &result.unindexed_files {
                            println!("  {}", f);
                        }
                    }
                    if !result.impact.is_empty() {
                        println!("\nTransitive impact:");
                        for entry in &result.impact {
                            println!(
                                "  <- {} {} ({})",
                                entry.symbol_kind, entry.symbol_name, entry.file_path
                            );
                        }
                    }
                    println!("\nSummary: {} files changed, {} symbols directly affected, {} transitively impacted",
                        result.summary.changed_files, result.summary.affected_symbols, result.summary.impacted_symbols);
                }
            }
        }
        Commands::Since { reference, depth } => {
            let result = astmap::ops::since(&project_dir, &reference, depth)?;
            if cli.json {
                println!("{}", serde_json::to_string_pretty(&result)?);
            } else if result.added.is_empty()
                && result.modified.is_empty()
                && result.removed.is_empty()
            {
                println!("No structural changes since {}", reference);
            } else {
                println!(
                    "Structural changes since {} ({} commits):\n",
                    result.reference, result.commit_count
                );
                if !result.added.is_empty() {
                    println!("Added:");
                    for sym in &result.added {
                        let parent = sym
                            .parent_name
                            .as_deref()
                            .map(|p| format!("{}.", p))
                            .unwrap_or_default();
                        println!(
                            "  + {}{} {} {}:{}",
                            parent, sym.name, sym.kind, sym.file_path, sym.line
                        );
                    }
                }
                if !result.modified.is_empty() {
                    println!("\nModified:");
                    for sym in &result.modified {
                        let parent = sym
                            .parent_name
                            .as_deref()
                            .map(|p| format!("{}.", p))
                            .unwrap_or_default();
                        println!(
                            "  ~ {}{} {} {}:{}",
                            parent, sym.name, sym.kind, sym.file_path, sym.line
                        );
                    }
                }
                if !result.removed.is_empty() {
                    println!("\nRemoved:");
                    for sym in &result.removed {
                        let parent = sym
                            .parent_name
                            .as_deref()
                            .map(|p| format!("{}.", p))
                            .unwrap_or_default();
                        println!(
                            "  - {}{} {} {}:{}",
                            parent, sym.name, sym.kind, sym.file_path, sym.line
                        );
                    }
                }
                if !result.downstream_impacts.is_empty() {
                    println!("\nDownstream impact:");
                    for entry in &result.downstream_impacts {
                        println!(
                            "  <- {} {} ({})",
                            entry.symbol_kind, entry.symbol_name, entry.file_path
                        );
                    }
                }
                println!(
                    "\nSummary: +{} added, ~{} modified, -{} removed, {} downstream impacted",
                    result.summary.added_count,
                    result.summary.modified_count,
                    result.summary.removed_count,
                    result.summary.impact_count
                );
            }
        }
        Commands::Digest { depth } => {
            let result = astmap::ops::digest(&project_dir, depth)?;
            let since_result = &result.since;
            if cli.json {
                let mut output = serde_json::to_value(since_result)?;
                if let Some(uc) = &result.uncommitted {
                    output["uncommitted_changes"] = serde_json::to_value(uc)?;
                }
                println!("{}", serde_json::to_string_pretty(&output)?);
            } else {
                println!(
                    "[astmap digest] Since last session ({} commits from {}):",
                    since_result.commit_count, since_result.reference
                );
                println!(
                    "Symbols: +{} added, ~{} modified, -{} removed",
                    since_result.summary.added_count,
                    since_result.summary.modified_count,
                    since_result.summary.removed_count,
                );
                if since_result.summary.impact_count > 0 {
                    println!(
                        "Impact radius: {} downstream symbols affected",
                        since_result.summary.impact_count
                    );
                }
                if let Some(uc) = &result.uncommitted {
                    if !uc.changed_files.is_empty() {
                        println!(
                            "Uncommitted: {} files, {} symbols affected",
                            uc.summary.changed_files, uc.summary.affected_symbols
                        );
                    }
                }
                if since_result.summary.added_count
                    + since_result.summary.modified_count
                    + since_result.summary.removed_count
                    > 0
                {
                    println!(
                        "Run `astmap since {}` for full details.",
                        since_result.reference
                    );
                }
            }
        }
        Commands::Export {
            output,
            token_budget,
        } => {
            let md = astmap::ops::export_md(&project_dir, None, token_budget)?;
            if let Some(path) = output {
                std::fs::write(&path, &md)?;
                println!("Exported to {}", path.display());
            } else {
                print!("{}", md);
            }
        }
        Commands::Mcp => {
            crate::mcp::run_mcp_server(project_dir).await?;
        }
        Commands::Watch => {
            let result = astmap::ops::scan(&project_dir, false)?;
            println!(
                "Initial scan: {} files, {} symbols, {} deps",
                result.files_total + result.files_skipped,
                result.symbols,
                result.dependencies
            );
            println!("Watching for changes... (Ctrl+C to stop)");
            astmap::ops::watch(&project_dir)?;
        }
        Commands::Daemon { action } => match action {
            DaemonAction::Start => {
                println!("Starting daemon... (Ctrl+C to stop)");
                astmap::ops::daemon_start(&project_dir).await?;
            }
            DaemonAction::Stop => {
                astmap::ops::daemon_stop(&project_dir)?;
                println!("Daemon stopped.");
            }
            DaemonAction::Status => {
                let st = astmap::ops::daemon_status(&project_dir);
                if cli.json {
                    println!("{}", serde_json::to_string_pretty(&st)?);
                } else if st.running {
                    println!(
                        "Daemon running (pid: {}, socket: {})",
                        st.pid.unwrap_or(0),
                        st.socket
                            .as_deref()
                            .unwrap_or(std::path::Path::new("-"))
                            .display()
                    );
                } else {
                    println!("Daemon not running.");
                }
            }
        },
        Commands::Subscribe {
            filter,
            json,
            pretty,
        } => {
            astmap::ops::subscribe(&project_dir, filter.as_deref(), json, pretty).await?;
        }
        Commands::Config { action } => match action {
            ConfigAction::Get { key } => match astmap::ops::config_get(&project_dir, &key)? {
                Some(v) => println!("{}", v),
                None => eprintln!("Config key not found: {}", key),
            },
            ConfigAction::Set { key, value } => {
                astmap::ops::config_set(&project_dir, &key, &value)?;
                println!("Set {} = {}", key, value);
            }
            ConfigAction::List => {
                let configs = astmap::ops::config_list(&project_dir)?;
                if cli.json {
                    println!("{}", serde_json::to_string_pretty(&configs)?);
                } else {
                    for c in &configs {
                        println!("{} = {}", c.key, c.value);
                    }
                }
            }
        },
    }

    Ok(())
}

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

    fn setup_project() -> tempfile::TempDir {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(
            src.join("main.rs"),
            r#"use std::io;
fn main() { helper(); }
fn helper() -> i32 { 42 }
struct Config { x: i32 }
impl Config {
    fn new(x: i32) -> Self { Config { x } }
}
fn standalone() -> i32 { 99 }
"#,
        )
        .unwrap();
        astmap::ops::scan(tmp.path(), true).unwrap();
        tmp
    }

    fn setup_git_project() -> tempfile::TempDir {
        let tmp = setup_project();
        let repo = git2::Repository::init(tmp.path()).unwrap();
        let mut index = repo.index().unwrap();
        index.add_path(std::path::Path::new("src/main.rs")).unwrap();
        index.write().unwrap();
        let oid = index.write_tree().unwrap();
        let tree = repo.find_tree(oid).unwrap();
        let sig = git2::Signature::now("Test", "test@test.com").unwrap();
        repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
            .unwrap();
        tmp
    }

    fn cli_from(args: &[&str]) -> Cli {
        Cli::try_parse_from(args).unwrap()
    }

    #[tokio::test]
    async fn test_cmd_scan() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("main.rs"), "fn main() {}").unwrap();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "scan"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_scan_full_json() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("main.rs"), "fn main() {}").unwrap();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "scan", "--full"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_query() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "query", "main"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_query_json() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "query", "helper"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_inspect_by_name() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "inspect", "Config"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_inspect_by_id() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "inspect", "1"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_inspect_json() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "inspect", "main"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_inspect_not_found() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "inspect", "nonexistent"]);
        let result = run_command(cli).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_cmd_impact() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "impact", "helper"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_impact_json() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "impact", "main"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_impact_not_found() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "impact", "nonexistent"]);
        let result = run_command(cli).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_cmd_why() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "why", "main", "helper"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_why_json() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "why", "main", "helper"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_why_not_found() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "why", "nonexistent", "helper"]);
        let result = run_command(cli).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_cmd_diff_working_tree() {
        let tmp = setup_git_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "diff"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_diff_json() {
        let tmp = setup_git_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "diff"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_diff_with_changes() {
        let tmp = setup_git_project();
        fs::write(
            tmp.path().join("src/main.rs"),
            "fn main() { println!(\"changed\"); }\n",
        )
        .unwrap();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "diff"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_export_stdout() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "export"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_export_to_file() {
        let tmp = setup_project();
        let out_file = tmp.path().join("output.md");
        let path = tmp.path().to_str().unwrap();
        let out_str = out_file.to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "export", "-o", out_str]);
        run_command(cli).await.unwrap();
        assert!(out_file.exists());
    }

    #[tokio::test]
    async fn test_cmd_config_set_get_list() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();

        let cli = cli_from(&["astmap", "-p", path, "config", "set", "test.key", "val"]);
        run_command(cli).await.unwrap();

        let cli = cli_from(&["astmap", "-p", path, "config", "get", "test.key"]);
        run_command(cli).await.unwrap();

        let cli = cli_from(&["astmap", "-p", path, "config", "get", "missing"]);
        run_command(cli).await.unwrap();

        let cli = cli_from(&["astmap", "-p", path, "config", "list"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_inspect_with_children() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "inspect", "Config"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_why_no_path() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "why", "standalone", "Config"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_why_to_not_found() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "why", "main", "nonexistent"]);
        let result = run_command(cli).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_cmd_diff_with_unindexed() {
        let tmp = setup_git_project();
        fs::write(tmp.path().join("src/new_module.rs"), "fn brand_new() {}").unwrap();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "diff"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_export_with_budget() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "export", "--token-budget", "5000"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_config_list_json() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "config", "set", "k", "v"]);
        run_command(cli).await.unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "config", "list"]);
        run_command(cli).await.unwrap();
    }

    // --- Two-commit git project for since/digest tests ---

    fn setup_two_commit_project() -> tempfile::TempDir {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();

        // First commit: initial code
        fs::write(
            src.join("main.rs"),
            "fn main() { helper(); }\nfn helper() -> i32 { 42 }\n",
        )
        .unwrap();
        let repo = git2::Repository::init(tmp.path()).unwrap();
        let mut index = repo.index().unwrap();
        index.add_path(std::path::Path::new("src/main.rs")).unwrap();
        index.write().unwrap();
        let oid = index.write_tree().unwrap();
        let tree = repo.find_tree(oid).unwrap();
        let sig = git2::Signature::now("Test", "test@test.com").unwrap();
        let commit1 = repo
            .commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
            .unwrap();
        let parent = repo.find_commit(commit1).unwrap();

        // Second commit: add a new function and modify helper
        fs::write(
            src.join("main.rs"),
            "fn main() { helper(); new_fn(); }\nfn helper(x: i32) -> i32 { x }\nfn new_fn() -> bool { true }\n",
        )
        .unwrap();
        let mut index = repo.index().unwrap();
        index.add_path(std::path::Path::new("src/main.rs")).unwrap();
        index.write().unwrap();
        let oid2 = index.write_tree().unwrap();
        let tree2 = repo.find_tree(oid2).unwrap();
        repo.commit(Some("HEAD"), &sig, &sig, "add new_fn", &tree2, &[&parent])
            .unwrap();

        astmap::ops::scan(tmp.path(), true).unwrap();
        tmp
    }

    #[tokio::test]
    async fn test_cmd_since_human() {
        let tmp = setup_two_commit_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "since", "HEAD~1"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_since_json() {
        let tmp = setup_two_commit_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "since", "HEAD~1"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_since_no_changes() {
        let tmp = setup_two_commit_project();
        let path = tmp.path().to_str().unwrap();
        // HEAD vs HEAD → no changes
        let cli = cli_from(&["astmap", "-p", path, "since", "HEAD"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_digest_human() {
        let tmp = setup_two_commit_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "digest"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_digest_json() {
        let tmp = setup_two_commit_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "digest"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_diff_with_base() {
        let tmp = setup_two_commit_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "diff", "--base", "HEAD~1"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_diff_with_base_json() {
        let tmp = setup_two_commit_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "diff", "--base", "HEAD~1"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_impact_no_dependents() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        // standalone has no callers
        let cli = cli_from(&["astmap", "-p", path, "impact", "standalone"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_impact_with_dependents() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        // helper is called by main → should show dependents
        let cli = cli_from(&["astmap", "-p", path, "impact", "helper"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_diff_no_changes_human() {
        let tmp = setup_git_project();
        let path = tmp.path().to_str().unwrap();
        // No uncommitted changes
        let cli = cli_from(&["astmap", "-p", path, "diff"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_diff_with_impact() {
        let tmp = setup_git_project();
        // Modify helper → should show transitive impact on main
        fs::write(
            tmp.path().join("src/main.rs"),
            r#"use std::io;
fn main() { helper(); }
fn helper(x: i32) -> i32 { x }
struct Config { x: i32 }
impl Config {
    fn new(x: i32) -> Self { Config { x } }
}
fn standalone() -> i32 { 99 }
"#,
        )
        .unwrap();
        // Re-scan to update the index
        astmap::ops::scan(tmp.path(), true).unwrap();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "diff"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_daemon_status() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "daemon", "status"]);
        run_command(cli).await.unwrap();
    }

    #[tokio::test]
    async fn test_cmd_daemon_status_json() {
        let tmp = setup_project();
        let path = tmp.path().to_str().unwrap();
        let cli = cli_from(&["astmap", "-p", path, "--json", "daemon", "status"]);
        run_command(cli).await.unwrap();
    }
}
