//! Info command implementation.

use crate::cli::InfoArgs;
use crate::config;
use crate::error::Result;
use crate::output::{OutputContext, OutputMode};
use crate::util::parse_id;
use fsqlite::Connection;
use fsqlite_types::SqliteValue;
use rich_rust::prelude::*;
use serde::Serialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::debug;

#[derive(Serialize)]
struct SchemaInfo {
    tables: Vec<String>,
    schema_version: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    config: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    sample_issue_ids: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    detected_prefix: Option<String>,
}

#[derive(Serialize)]
struct InfoOutput {
    database_path: String,
    beads_dir: String,
    mode: String,
    daemon_connected: bool,
    #[serde(skip)]
    resolved_prefix: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    daemon_fallback_reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    daemon_detail: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    issue_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    config: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    schema: Option<SchemaInfo>,
    #[serde(skip_serializing_if = "Option::is_none")]
    db_size: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    jsonl_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    jsonl_size: Option<u64>,
}

#[derive(Default)]
struct InfoSnapshot {
    issue_count: Option<usize>,
    config_map: Option<HashMap<String, String>>,
    detected_prefix: Option<String>,
    schema: Option<SchemaInfo>,
}

/// Execute the info command.
///
/// # Errors
///
/// Returns an error if configuration or storage access fails.
pub fn execute(args: &InfoArgs, cli: &config::CliOverrides, ctx: &OutputContext) -> Result<()> {
    if args.whats_new {
        return print_message(ctx, "No whats-new data available for br.", "whats_new");
    }
    if args.thanks {
        return print_message(
            ctx,
            "Thanks for using br. See README for project acknowledgements.",
            "thanks",
        );
    }

    let output = collect_info_output(args, cli)?;

    if matches!(ctx.mode(), OutputMode::Quiet) {
        return Ok(());
    }

    if ctx.is_json() {
        ctx.json_pretty(&output);
        return Ok(());
    }

    if ctx.is_toon() {
        ctx.toon(&output);
        return Ok(());
    }

    if ctx.is_rich() {
        render_info_rich(&output, ctx);
    } else {
        print_human(&output);
    }

    Ok(())
}

fn collect_info_output(args: &InfoArgs, cli: &config::CliOverrides) -> Result<InfoOutput> {
    let beads_dir = config::discover_beads_dir_with_cli(cli)?;
    let startup = config::load_startup_config_with_paths(&beads_dir, cli.db.as_ref())?;
    let snapshot = load_info_snapshot_without_recovery(args, &startup.paths);
    let resolved_prefix = config::configured_issue_prefix_from_map(&startup.merged_config.runtime)
        .or_else(|| snapshot.detected_prefix.clone())
        .or_else(|| {
            config::first_prefix_from_jsonl(&startup.paths.jsonl_path)
                .ok()
                .flatten()
        });

    let db_path = canonicalize_lossy(&startup.paths.db_path);
    let db_size = std::fs::metadata(&startup.paths.db_path)
        .map(|metadata| metadata.len())
        .ok();
    let jsonl_size = std::fs::metadata(&startup.paths.jsonl_path)
        .map(|metadata| metadata.len())
        .ok();

    Ok(InfoOutput {
        database_path: db_path.display().to_string(),
        beads_dir: canonicalize_lossy(&beads_dir).display().to_string(),
        mode: "direct".to_string(),
        daemon_connected: false,
        resolved_prefix,
        daemon_fallback_reason: Some("no-daemon".to_string()),
        daemon_detail: Some("br runs in direct mode only".to_string()),
        issue_count: snapshot.issue_count,
        config: snapshot.config_map,
        schema: snapshot.schema,
        db_size,
        jsonl_path: Some(
            canonicalize_lossy(&startup.paths.jsonl_path)
                .display()
                .to_string(),
        ),
        jsonl_size,
    })
}

fn load_info_snapshot_without_recovery(
    args: &InfoArgs,
    paths: &config::ConfigPaths,
) -> InfoSnapshot {
    if !paths.db_path.is_file() {
        return InfoSnapshot::default();
    }

    match config::with_database_family_snapshot(&paths.db_path, |snapshot_db_path| {
        let conn = Connection::open(snapshot_db_path.to_string_lossy().into_owned())?;
        let issue_count = query_issue_count(&conn);
        let config_map = load_config_map(&conn);
        let detected_prefix = detect_prefix(&conn, config_map.as_ref());
        let schema = if args.schema {
            Some(build_schema_info(
                &conn,
                config_map.as_ref(),
                detected_prefix.clone(),
            ))
        } else {
            None
        };

        Ok(InfoSnapshot {
            issue_count,
            config_map,
            detected_prefix,
            schema,
        })
    }) {
        Ok(snapshot) => snapshot,
        Err(err) => {
            debug!(
                path = %paths.db_path.display(),
                error = %err,
                "Skipping DB-backed info details because the database could not be snapshotted for read-only access"
            );
            InfoSnapshot::default()
        }
    }
}

fn query_issue_count(conn: &Connection) -> Option<usize> {
    conn.query_row("SELECT COUNT(*) FROM issues")
        .ok()
        .and_then(|row| row.get(0).and_then(SqliteValue::as_integer))
        .and_then(|count| usize::try_from(count).ok())
}

fn load_config_map(conn: &Connection) -> Option<HashMap<String, String>> {
    let rows = conn.query("SELECT key, value FROM config").ok()?;
    let mut config_map = HashMap::new();

    for row in rows {
        let Some(key) = row.get(0).and_then(SqliteValue::as_text) else {
            continue;
        };
        let Some(value) = row.get(1).and_then(SqliteValue::as_text) else {
            continue;
        };
        config_map.insert(key.to_string(), value.to_string());
    }

    (!config_map.is_empty()).then_some(config_map)
}

fn build_schema_info(
    conn: &Connection,
    config_map: Option<&HashMap<String, String>>,
    detected_prefix: Option<String>,
) -> SchemaInfo {
    let tables = actual_table_names(conn);
    let sample_issue_ids: Vec<String> = conn
        .query("SELECT id FROM issues ORDER BY id LIMIT 3")
        .ok()
        .map(|rows| {
            rows.into_iter()
                .filter_map(|row| {
                    row.get(0)
                        .and_then(SqliteValue::as_text)
                        .map(str::to_string)
                })
                .collect()
        })
        .unwrap_or_default();

    SchemaInfo {
        tables,
        schema_version: actual_schema_version(conn),
        config: config_map.cloned(),
        sample_issue_ids,
        detected_prefix,
    }
}

fn detect_prefix(
    conn: &Connection,
    config_map: Option<&HashMap<String, String>>,
) -> Option<String> {
    config_map
        .and_then(config::configured_issue_prefix_from_map)
        .or_else(|| {
            conn.query("SELECT id FROM issues ORDER BY id LIMIT 1")
                .ok()
                .and_then(|rows| rows.first().cloned())
                .and_then(|row| {
                    row.get(0)
                        .and_then(SqliteValue::as_text)
                        .map(str::to_string)
                })
                .and_then(|id| parse_id(&id).ok().map(|parsed| parsed.prefix))
        })
}

fn actual_table_names(conn: &Connection) -> Vec<String> {
    conn.query(
        "SELECT name FROM sqlite_master \
         WHERE type = 'table' AND name NOT LIKE 'sqlite_%' \
         ORDER BY name",
    )
    .ok()
    .map(|rows| {
        rows.into_iter()
            .filter_map(|row| {
                row.get(0)
                    .and_then(SqliteValue::as_text)
                    .map(str::to_string)
            })
            .collect()
    })
    .unwrap_or_default()
}

fn actual_schema_version(conn: &Connection) -> String {
    conn.query_row("PRAGMA user_version")
        .ok()
        .and_then(|row| row.get(0).and_then(SqliteValue::as_integer))
        .map_or_else(|| "unknown".to_string(), |version| version.to_string())
}

fn print_human(info: &InfoOutput) {
    println!("Beads Database Information");
    println!("Beads dir: {}", info.beads_dir);
    println!("Database: {}", info.database_path);
    if let Some(size) = info.db_size {
        println!("Database size: {}", format_bytes(size));
    }
    if let Some(jsonl_path) = &info.jsonl_path {
        println!("JSONL: {jsonl_path}");
        if let Some(size) = info.jsonl_size {
            println!("JSONL size: {}", format_bytes(size));
        }
    }
    println!("Mode: {}", info.mode);

    if info.daemon_connected {
        println!("Daemon: connected");
    } else if let Some(reason) = &info.daemon_fallback_reason {
        println!("Daemon: not connected ({reason})");
        if let Some(detail) = &info.daemon_detail {
            println!("  {detail}");
        }
    }

    if let Some(count) = info.issue_count {
        println!("Issue count: {count}");
    }

    if let Some(prefix) = info.resolved_prefix.as_deref() {
        println!("Issue prefix: {prefix}");
    }

    if let Some(schema) = &info.schema {
        println!();
        println!("Schema:");
        println!("  Version: {}", schema.schema_version);
        println!("  Tables: {}", schema.tables.join(", "));
        if let Some(prefix) = &schema.detected_prefix {
            println!("  Detected prefix: {prefix}");
        }
        if !schema.sample_issue_ids.is_empty() {
            println!("  Sample IDs: {}", schema.sample_issue_ids.join(", "));
        }
    }
}

#[allow(clippy::unnecessary_wraps)]
fn print_message(ctx: &OutputContext, message: &str, key: &str) -> Result<()> {
    if ctx.is_json() {
        let payload = serde_json::json!({ key: message });
        ctx.json_pretty(&payload);
    } else if ctx.is_toon() {
        let payload = serde_json::json!({ key: message });
        ctx.toon(&payload);
    } else if matches!(ctx.mode(), OutputMode::Quiet) {
        return Ok(());
    } else if ctx.is_rich() {
        let console = Console::default();
        let theme = ctx.theme();
        let text = Text::styled(message, theme.muted.clone());
        console.print_renderable(&text);
    } else {
        println!("{message}");
    }
    Ok(())
}

/// Render project info as a rich panel.
fn render_info_rich(info: &InfoOutput, ctx: &OutputContext) {
    let console = Console::default();
    let theme = ctx.theme();
    let width = ctx.width();

    let mut content = Text::new("");

    // Location section
    content.append_styled("Location    ", theme.dimmed.clone());
    content.append_styled(&info.beads_dir, theme.accent.clone());
    content.append("\n");

    // Prefix (if available)
    if let Some(prefix) = info.resolved_prefix.as_deref() {
        content.append_styled("Prefix      ", theme.dimmed.clone());
        content.append_styled(prefix, theme.issue_id.clone());
        content.append("\n");
    }

    content.append("\n");

    // Database section
    content.append_styled("Database\n", theme.section.clone());
    content.append_styled("  Path      ", theme.dimmed.clone());
    content.append_styled(&info.database_path, theme.accent.clone());
    content.append("\n");

    if let Some(size) = info.db_size {
        content.append_styled("  Size      ", theme.dimmed.clone());
        content.append(&format_bytes(size));
        content.append("\n");
    }

    if let Some(count) = info.issue_count {
        content.append_styled("  Issues    ", theme.dimmed.clone());
        content.append_styled(&format!("{count}"), theme.emphasis.clone());
        content.append_styled(" total\n", theme.dimmed.clone());
    }

    // JSONL section
    if let Some(jsonl_path) = &info.jsonl_path {
        content.append("\n");
        content.append_styled("JSONL\n", theme.section.clone());
        content.append_styled("  Path      ", theme.dimmed.clone());
        content.append_styled(jsonl_path, theme.accent.clone());
        content.append("\n");

        if let Some(size) = info.jsonl_size {
            content.append_styled("  Size      ", theme.dimmed.clone());
            content.append(&format_bytes(size));
            content.append("\n");
        }
    }

    // Mode section
    content.append("\n");
    content.append_styled("Mode        ", theme.dimmed.clone());
    content.append(&info.mode);
    if !info.daemon_connected {
        content.append_styled(" (no daemon)", theme.muted.clone());
    }
    content.append("\n");

    // Schema section (if requested)
    if let Some(schema) = &info.schema {
        content.append("\n");
        content.append_styled("Schema\n", theme.section.clone());
        content.append_styled("  Version   ", theme.dimmed.clone());
        content.append(&schema.schema_version);
        content.append("\n");

        content.append_styled("  Tables    ", theme.dimmed.clone());
        content.append(&schema.tables.join(", "));
        content.append("\n");

        if let Some(prefix) = &schema.detected_prefix {
            content.append_styled("  Prefix    ", theme.dimmed.clone());
            content.append_styled(prefix, theme.issue_id.clone());
            content.append("\n");
        }

        if !schema.sample_issue_ids.is_empty() {
            content.append_styled("  Samples   ", theme.dimmed.clone());
            content.append(&schema.sample_issue_ids.join(", "));
            content.append("\n");
        }
    }

    let panel = Panel::from_rich_text(&content, width)
        .title(Text::styled(
            "Project Information",
            theme.panel_title.clone(),
        ))
        .box_style(theme.box_style);

    console.print_renderable(&panel);
}

/// Format bytes as human-readable size.
fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        format!("{:.1} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{bytes} B")
    }
}

fn canonicalize_lossy(path: &Path) -> PathBuf {
    dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cli::InfoArgs;
    use crate::config::CliOverrides;
    use crate::storage::SqliteStorage;
    use crate::storage::schema::CURRENT_SCHEMA_VERSION;
    use std::env;
    use std::path::Path;
    use std::path::PathBuf;

    use tempfile::TempDir;

    struct DirGuard {
        previous: PathBuf,
    }

    impl DirGuard {
        fn new(target: &Path) -> Self {
            let previous = env::current_dir().unwrap_or_else(|_| PathBuf::from("/tmp"));
            env::set_current_dir(target).expect("set current dir");
            Self { previous }
        }
    }

    impl Drop for DirGuard {
        fn drop(&mut self) {
            let _ = env::set_current_dir(&self.previous);
        }
    }

    #[test]
    fn test_format_bytes_small() {
        assert_eq!(format_bytes(500), "500 B");
        assert_eq!(format_bytes(0), "0 B");
    }

    #[test]
    fn test_format_bytes_kb() {
        assert_eq!(format_bytes(1024), "1.0 KB");
        assert_eq!(format_bytes(1536), "1.5 KB");
    }

    #[test]
    fn test_format_bytes_mb() {
        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
        assert_eq!(format_bytes(2_500_000), "2.4 MB");
    }

    #[test]
    fn test_format_bytes_gb() {
        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
    }

    #[test]
    fn test_collect_info_output_does_not_create_missing_db() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(&InfoArgs::default(), &CliOverrides::default()).unwrap();

        assert!(
            !beads_dir.join("beads.db").exists(),
            "info collection should not create a missing database"
        );
        assert!(output.issue_count.is_none());
        assert_eq!(
            output.database_path,
            beads_dir.join("beads.db").display().to_string()
        );
    }

    #[test]
    fn test_collect_info_output_reads_existing_db_without_recovery() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();

        let db_path = beads_dir.join("beads.db");
        let mut storage = SqliteStorage::open(&db_path).unwrap();
        storage.set_config("issue_prefix", "bd").unwrap();
        let issue = crate::model::Issue {
            id: "bd-abc12".to_string(),
            title: "Example".to_string(),
            issue_type: crate::model::IssueType::Task,
            priority: crate::model::Priority::LOW,
            ..crate::model::Issue::default()
        };
        storage.create_issue(&issue, "test").unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(
            &InfoArgs {
                schema: true,
                ..InfoArgs::default()
            },
            &CliOverrides::default(),
        )
        .unwrap();

        assert_eq!(output.issue_count, Some(1));
        assert_eq!(
            output
                .config
                .as_ref()
                .and_then(|config| config.get("issue_prefix"))
                .map(String::as_str),
            Some("bd")
        );
        let expected_version = CURRENT_SCHEMA_VERSION.to_string();
        assert_eq!(
            output
                .schema
                .as_ref()
                .map(|schema| schema.schema_version.as_str()),
            Some(expected_version.as_str())
        );
        let expected_tables = vec![
            "blocked_issues_cache".to_string(),
            "child_counters".to_string(),
            "comments".to_string(),
            "config".to_string(),
            "dependencies".to_string(),
            "dirty_issues".to_string(),
            "events".to_string(),
            "export_hashes".to_string(),
            "issues".to_string(),
            "labels".to_string(),
            "metadata".to_string(),
        ];
        assert_eq!(
            output
                .schema
                .as_ref()
                .map(|schema| schema.tables.as_slice()),
            Some(expected_tables.as_slice())
        );
        assert_eq!(
            output
                .schema
                .as_ref()
                .and_then(|schema| schema.detected_prefix.as_deref()),
            Some("bd")
        );
    }

    #[test]
    fn test_collect_info_output_reports_actual_schema_snapshot() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();

        let db_path = beads_dir.join("beads.db");
        let conn = Connection::open(db_path.to_string_lossy().into_owned()).unwrap();
        conn.execute("CREATE TABLE issues (id TEXT PRIMARY KEY)")
            .unwrap();
        conn.execute("PRAGMA user_version = 1").unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(
            &InfoArgs {
                schema: true,
                ..InfoArgs::default()
            },
            &CliOverrides::default(),
        )
        .unwrap();

        let schema = output.schema.expect("schema details");
        assert_eq!(schema.schema_version, "1");
        assert_eq!(schema.tables, vec!["issues".to_string()]);
        assert!(schema.config.is_none());
        assert!(schema.sample_issue_ids.is_empty());
    }

    #[test]
    fn test_collect_info_output_prefers_startup_issue_prefix_over_db_config() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();
        std::fs::write(beads_dir.join("config.yaml"), "issue_prefix: proj\n").unwrap();

        let db_path = beads_dir.join("beads.db");
        let mut storage = SqliteStorage::open(&db_path).unwrap();
        storage.set_config("issue_prefix", "bd").unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(&InfoArgs::default(), &CliOverrides::default()).unwrap();

        assert_eq!(output.resolved_prefix.as_deref(), Some("proj"));
        assert_eq!(
            output
                .config
                .as_ref()
                .and_then(|config| config.get("issue_prefix"))
                .map(String::as_str),
            Some("bd"),
            "serialized DB config should remain unchanged"
        );
    }

    #[test]
    fn test_collect_info_output_detects_prefix_without_schema_flag() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();

        let db_path = beads_dir.join("beads.db");
        let mut storage = SqliteStorage::open(&db_path).unwrap();
        storage.set_config("issue_prefix", "proj").unwrap();
        let issue = crate::model::Issue {
            id: "proj-abc12".to_string(),
            title: "Example".to_string(),
            issue_type: crate::model::IssueType::Task,
            priority: crate::model::Priority::LOW,
            ..crate::model::Issue::default()
        };
        storage.create_issue(&issue, "test").unwrap();
        storage.delete_config("issue_prefix").unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(&InfoArgs::default(), &CliOverrides::default()).unwrap();

        assert_eq!(output.resolved_prefix.as_deref(), Some("proj"));
        assert!(output.schema.is_none());
        assert!(
            output
                .config
                .as_ref()
                .and_then(|config| config.get("issue_prefix"))
                .is_none()
        );
    }

    #[test]
    fn test_collect_info_output_uses_jsonl_prefix_when_db_is_missing() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();
        std::fs::write(
            beads_dir.join("issues.jsonl"),
            r#"{"id":"proj-abc12","title":"Example"}"#,
        )
        .unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(&InfoArgs::default(), &CliOverrides::default()).unwrap();

        assert_eq!(output.resolved_prefix.as_deref(), Some("proj"));
        assert!(output.issue_count.is_none());
        assert!(output.config.is_none());
        assert!(output.schema.is_none());
    }

    #[test]
    fn test_collect_info_output_accepts_startup_prefix_alias() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();
        std::fs::write(beads_dir.join("config.yaml"), "prefix: proj\n").unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(&InfoArgs::default(), &CliOverrides::default()).unwrap();

        assert_eq!(output.resolved_prefix.as_deref(), Some("proj"));
    }

    #[test]
    fn test_collect_info_output_accepts_db_prefix_alias() {
        let _lock = crate::util::test_helpers::TEST_DIR_LOCK
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let temp = TempDir::new().unwrap();
        let beads_dir = temp.path().join(".beads");
        std::fs::create_dir_all(&beads_dir).unwrap();
        std::fs::write(
            beads_dir.join("metadata.json"),
            r#"{"database":"beads.db","jsonl_export":"issues.jsonl"}"#,
        )
        .unwrap();

        let db_path = beads_dir.join("beads.db");
        let mut storage = SqliteStorage::open(&db_path).unwrap();
        storage.set_config("prefix", "proj").unwrap();

        let _guard = DirGuard::new(temp.path());

        let output = collect_info_output(&InfoArgs::default(), &CliOverrides::default()).unwrap();

        assert_eq!(output.resolved_prefix.as_deref(), Some("proj"));
        assert_eq!(
            output
                .config
                .as_ref()
                .and_then(|config| config.get("prefix"))
                .map(String::as_str),
            Some("proj")
        );
    }
}
