//! QA I/O — the ONLY place in `service::qa/` that may perform side effects.
//!
//! Side effects here include: spawning git processes via `std::process::Command`,
//! binding TCP sockets to discover free ports, and launching the QA app process.

use super::Error;

/// Find a free TCP port by attempting to bind starting from `start`.
///
/// Scans up to 100 ports above `start` and returns the first available one.
/// Falls back to `start` if none are found (unlikely in practice).
pub fn find_free_port(start: u16) -> u16 {
    for port in start..start + 100 {
        if std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() {
            return port;
        }
    }
    start
}

/// Merge a spike's feature branch into master using a fast-forward or simple merge.
///
/// Unlike the squash-merge pipeline used for stories, spikes skip quality gates
/// and preserve their commit history. Returns `true` if a merge was performed,
/// `false` if the branch had no unmerged commits (already up to date).
///
/// # Errors
/// - [`Error::Conflict`] if the merge fails due to conflicts.
/// - [`Error::UpstreamFailure`] if a git command cannot be run.
pub fn merge_spike_branch_to_master(
    project_root: &std::path::Path,
    branch: &str,
    story_id: &str,
) -> Result<bool, Error> {
    use std::process::Command;

    // Check the branch exists and has unmerged changes.
    if !crate::agents::lifecycle::feature_branch_has_unmerged_changes(project_root, story_id) {
        crate::slog!(
            "[qa] Spike '{story_id}': feature branch has no unmerged changes, skipping merge."
        );
        return Ok(false);
    }

    // Ensure we are on master.
    let checkout = Command::new("git")
        .args(["checkout", "master"])
        .current_dir(project_root)
        .output()
        .map_err(|e| Error::UpstreamFailure(format!("git checkout master failed: {e}")))?;
    if !checkout.status.success() {
        return Err(Error::UpstreamFailure(format!(
            "Failed to checkout master: {}",
            String::from_utf8_lossy(&checkout.stderr)
        )));
    }

    // Try fast-forward first, then fall back to a regular merge.
    let ff = Command::new("git")
        .args(["merge", "--ff-only", branch])
        .current_dir(project_root)
        .output()
        .map_err(|e| Error::UpstreamFailure(format!("git merge --ff-only failed: {e}")))?;

    if ff.status.success() {
        crate::slog!("[qa] Spike '{story_id}': fast-forward merged '{branch}' into master.");
        return Ok(true);
    }

    // Fast-forward failed (diverged history) — fall back to a regular merge.
    let merge = Command::new("git")
        .args([
            "merge",
            "--no-ff",
            branch,
            "-m",
            &format!("Merge spike branch '{branch}' into master"),
        ])
        .current_dir(project_root)
        .output()
        .map_err(|e| Error::UpstreamFailure(format!("git merge failed: {e}")))?;

    if merge.status.success() {
        crate::slog!("[qa] Spike '{story_id}': merged '{branch}' into master (no-ff).");
        Ok(true)
    } else {
        Err(Error::Conflict(format!(
            "Failed to merge spike branch '{branch}' into master: {}",
            String::from_utf8_lossy(&merge.stderr)
        )))
    }
}
