//! Async worktree removal operations.
use crate::config::ProjectConfig;
use std::path::Path;

use super::create::run_teardown_commands;
use super::git::{branch_name, remove_worktree_sync, resolve_base_branch};
use super::{WorktreeInfo, worktree_path};

/// Remove a git worktree and its branch.
///
/// Story 1199: measures the worktree's `target/` size before removal (once
/// removed, there's nothing left to measure) and logs bytes reclaimed on
/// success, so terminal-story worktree cleanup reports its disk impact the
/// same way the on-demand `gc` pass does.
pub async fn remove_worktree(
    project_root: &Path,
    info: &WorktreeInfo,
    config: &ProjectConfig,
) -> Result<(), String> {
    run_teardown_commands(&info.path, config).await?;

    let target_path = info.path.join("target");
    let target_bytes = tokio::task::spawn_blocking(move || {
        crate::service::disk_watch::io::dir_size_bytes(&target_path)
    })
    .await
    .unwrap_or(0);

    let root = project_root.to_path_buf();
    let wt_path = info.path.clone();
    let branch = info.branch.clone();

    let result =
        tokio::task::spawn_blocking(move || remove_worktree_sync(&root, &wt_path, &branch))
            .await
            .map_err(|e| format!("spawn_blocking: {e}"))?;

    if result.is_ok() && target_bytes > 0 {
        crate::slog!(
            "[worktree-remove] Reclaimed {target_bytes} bytes from '{}' target/ on removal",
            info.path.display()
        );
    }

    result
}

/// Remove a git worktree by story ID, deriving the path and branch deterministically.
pub async fn remove_worktree_by_story_id(
    project_root: &Path,
    story_id: &str,
    config: &ProjectConfig,
) -> Result<(), String> {
    let path = worktree_path(project_root, story_id);
    if !path.exists() {
        return Err(format!("Worktree not found for story: {story_id}"));
    }
    let branch = branch_name(story_id);
    let base_branch = resolve_base_branch(project_root, config.base_branch.as_deref());
    let info = WorktreeInfo {
        path,
        branch,
        base_branch,
    };
    remove_worktree(project_root, &info, config).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::WatcherConfig;
    use std::fs;
    use std::process::Command;
    use tempfile::TempDir;

    fn init_git_repo(dir: &std::path::Path) {
        Command::new("git")
            .args(["init"])
            .current_dir(dir)
            .output()
            .expect("git init");
        Command::new("git")
            .args(["commit", "--allow-empty", "-m", "init"])
            .current_dir(dir)
            .output()
            .expect("git commit");
    }

    fn empty_config() -> ProjectConfig {
        ProjectConfig {
            component: vec![],
            agent: vec![],
            watcher: WatcherConfig::default(),
            disk_watch: Default::default(),
            default_qa: "server".to_string(),
            default_coder_model: None,
            max_coders: None,
            max_retries: 2,
            base_branch: None,
            rate_limit_notifications: true,
            web_ui_status_consumer: true,
            matrix_status_consumer: true,
            slack_status_consumer: true,
            discord_status_consumer: true,
            whatsapp_status_consumer: true,
            timezone: None,
            rendezvous: None,
            trusted_keys: Vec::new(),
            crdt_require_token: false,
            crdt_tokens: Vec::new(),
            max_mesh_peers: 3,
            gateway_url: None,
            gateway_project: None,
            status_push_enabled: true,
            merge_failure_block_threshold: 3,
            gc_min_free_gb: 0,
        }
    }

    #[tokio::test]
    async fn remove_worktree_by_story_id_returns_err_when_not_found() {
        let tmp = TempDir::new().unwrap();

        let result =
            remove_worktree_by_story_id(tmp.path(), "99_nonexistent", &empty_config()).await;
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .contains("Worktree not found for story: 99_nonexistent")
        );
    }

    #[tokio::test]
    async fn remove_worktree_by_story_id_removes_existing_worktree() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        super::super::create::create_worktree(
            &project_root,
            "88_remove_by_id",
            &empty_config(),
            3001,
        )
        .await
        .unwrap();

        let result =
            remove_worktree_by_story_id(&project_root, "88_remove_by_id", &empty_config()).await;
        assert!(
            result.is_ok(),
            "Expected removal to succeed: {:?}",
            result.err()
        );
    }

    #[tokio::test]
    async fn remove_worktree_by_story_id_cleans_git_metadata_and_branch() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        let story_id = "89_regression_remove";
        let expected_branch = format!("feature/story-{story_id}");

        super::super::create::create_worktree(&project_root, story_id, &empty_config(), 3001)
            .await
            .unwrap();

        let wt_path = super::super::worktree_path(&project_root, story_id);
        assert!(wt_path.exists(), "worktree must exist before removal");

        let result = remove_worktree_by_story_id(&project_root, story_id, &empty_config()).await;
        assert!(
            result.is_ok(),
            "Expected removal to succeed: {:?}",
            result.err()
        );

        // Regression: `git worktree list` must not show the removed worktree.
        let wt_list_output = std::process::Command::new("git")
            .args(["worktree", "list", "--porcelain"])
            .current_dir(&project_root)
            .output()
            .expect("git worktree list");
        let wt_list = String::from_utf8_lossy(&wt_list_output.stdout);
        assert!(
            !wt_list.contains(&*wt_path.to_string_lossy()),
            "git worktree list should not contain the removed worktree path, but got:\n{wt_list}"
        );

        // Regression: feature branch must be deleted after worktree removal.
        let branch_output = std::process::Command::new("git")
            .args(["branch", "--list", &expected_branch])
            .current_dir(&project_root)
            .output()
            .expect("git branch --list");
        let branch_list = String::from_utf8_lossy(&branch_output.stdout);
        assert!(
            branch_list.trim().is_empty(),
            "Feature branch '{expected_branch}' should be deleted, but found: {branch_list}"
        );
    }

    #[tokio::test]
    async fn remove_worktree_async_removes_directory() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        let info = super::super::create::create_worktree(
            &project_root,
            "77_remove_async",
            &empty_config(),
            3001,
        )
        .await
        .unwrap();

        let path = info.path.clone();
        assert!(path.exists());
        remove_worktree(&project_root, &info, &empty_config())
            .await
            .unwrap();
        assert!(!path.exists());
    }

    /// Story 1199 AC4: removal reclaims a populated `target/` dir along with
    /// the rest of the worktree.
    #[tokio::test]
    async fn remove_worktree_reclaims_populated_target_dir() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        let info = super::super::create::create_worktree(
            &project_root,
            "90_reclaim_target",
            &empty_config(),
            3001,
        )
        .await
        .unwrap();

        let target_dir = info.path.join("target");
        fs::create_dir_all(&target_dir).unwrap();
        fs::write(target_dir.join("build_artifact.bin"), vec![0u8; 1024]).unwrap();
        assert!(target_dir.exists());

        remove_worktree(&project_root, &info, &empty_config())
            .await
            .unwrap();

        assert!(!info.path.exists(), "whole worktree must be gone");
        assert!(!target_dir.exists(), "target/ must be reclaimed");
    }
}
