//! Merge operations — rebases agent work onto master and runs post-merge validation.

use serde::{Deserialize, Serialize};

mod squash;

pub(crate) use squash::{merge_lock_is_free, run_squash_merge};

/// Typed outcome of a completed squash-merge operation.
///
/// Each variant captures only the fields relevant to that outcome, eliminating
/// the four-bool soup of the old `MergeReport`.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "kind")]
pub enum MergeResult {
    /// Squash commit landed on the base branch and all quality gates passed.
    Success {
        /// `true` when conflicts were detected and automatically resolved.
        conflicts_resolved: bool,
        conflict_details: Option<String>,
        /// Human-readable output from the quality-gate run.
        gate_output: String,
    },
    /// Merge was aborted due to unresolvable conflicts; base branch is untouched.
    Conflict {
        details: Option<String>,
        output: String,
    },
    /// Squash commit produced but quality gates failed; base branch may carry the commit.
    GateFailure {
        output: String,
        #[serde(default)]
        failure_kind: Option<crate::agents::gates::GateFailureKind>,
    },
    /// Feature branch had zero commits ahead of the base branch.
    NoCommits { output: String },
    /// Unclassified failure (cherry-pick failed, git error, etc.).
    Other {
        output: String,
        conflict_details: Option<String>,
    },
}

impl MergeResult {
    /// Extract the human-readable output string from any variant.
    pub fn output(&self) -> &str {
        match self {
            Self::Success { gate_output, .. } => gate_output,
            Self::Conflict { output, .. }
            | Self::GateFailure { output, .. }
            | Self::NoCommits { output }
            | Self::Other { output, .. } => output,
        }
    }

    /// Convert a non-success outcome into the pipeline-level `MergeFailureKind`
    /// used to drive recovery routing.
    ///
    /// A `GateFailure` whose typed `failure_kind` is `Build` is reclassified as
    /// `ConflictDetected` — a post-squash compile error after a clean git merge
    /// is semantically a merge conflict that git's diff3 missed (the conflicting
    /// literal lives in a different file from the type definition that changed
    /// on master). Routing it as a conflict ensures mergemaster auto-spawns to
    /// resolve it, rather than the failure sitting as an opaque `GatesFailed`.
    ///
    /// Panics on `Success`; callers must guard with a success check first.
    pub fn to_merge_failure_kind(&self) -> crate::pipeline_state::MergeFailureKind {
        use crate::pipeline_state::MergeFailureKind;
        match self {
            Self::Success { .. } => {
                panic!("to_merge_failure_kind called on MergeResult::Success")
            }
            Self::NoCommits { .. } => MergeFailureKind::NoCommits,
            Self::Conflict { details, .. } => MergeFailureKind::ConflictDetected(details.clone()),
            Self::GateFailure {
                output,
                failure_kind: Some(crate::agents::gates::GateFailureKind::Build),
            } => MergeFailureKind::ConflictDetected(Some(output.clone())),
            Self::GateFailure { output, .. } => MergeFailureKind::GatesFailed(output.clone()),
            Self::Other { output, .. } => MergeFailureKind::Other(output.clone()),
        }
    }
}

/// Status of an async merge job.
#[derive(Debug, Clone, Serialize)]
pub enum MergeJobStatus {
    Running,
    Completed(MergeReport),
    Failed(String),
}

/// Tracks a background merge job started by `merge_agent_work`.
#[derive(Debug, Clone, Serialize)]
pub struct MergeJob {
    pub story_id: String,
    pub status: MergeJobStatus,
    /// Server start-time (Unix seconds) of the server instance that started
    /// this job.
    ///
    /// Used by stale-lock recovery: on a new merge attempt the system checks
    /// every Running entry and removes any whose recorded start-time is older
    /// than the current server's boot time. This survives `rebuild_and_restart`
    /// (which re-execs and keeps the same PID).
    pub server_start_time: f64,
    /// Unix timestamp (seconds) when this merge job started, used to compute
    /// elapsed time for a still-`Running` job.
    pub started_at: f64,
}

/// Result of a mergemaster merge operation.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MergeReport {
    pub story_id: String,
    /// Typed outcome of the merge operation.
    pub result: MergeResult,
    pub worktree_cleaned_up: bool,
    pub story_archived: bool,
    /// Path (relative to the project root) of the full untruncated report
    /// written by `service::merge::io::write_merge_report`, if the write
    /// succeeded.
    #[serde(default)]
    pub report_path: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agents::gates::GateFailureKind;
    use crate::pipeline_state::MergeFailureKind;

    #[test]
    fn to_failure_kind_maps_build_gate_failure_to_conflict_detected() {
        // Post-squash compile error (e.g. story 1018: `error[E0063]: missing field
        // plan` because master gained Stage::Coding's plan field after the
        // coder's branch was committed) is a semantic merge conflict that git
        // missed. Mergemaster should auto-spawn, so the kind must be
        // ConflictDetected, not GatesFailed.
        let output = "error[E0063]: missing field `plan` in initializer of `Stage`".to_string();
        let result = MergeResult::GateFailure {
            output: output.clone(),
            failure_kind: Some(GateFailureKind::Build),
        };
        match result.to_merge_failure_kind() {
            MergeFailureKind::ConflictDetected(details) => {
                assert_eq!(details.as_deref(), Some(output.as_str()));
            }
            other => panic!("expected ConflictDetected, got {other:?}"),
        }
    }

    #[test]
    fn to_failure_kind_maps_test_gate_failure_to_gates_failed() {
        // Non-build gate failures (test failure, fmt drift, lint, etc.) are
        // genuine gate problems, not merge conflicts. They must remain
        // GatesFailed so mergemaster does NOT auto-spawn for them.
        let result = MergeResult::GateFailure {
            output: "test result: FAILED. 1 failed".to_string(),
            failure_kind: Some(GateFailureKind::Test),
        };
        assert!(matches!(
            result.to_merge_failure_kind(),
            MergeFailureKind::GatesFailed(_)
        ));
    }

    #[test]
    fn to_failure_kind_maps_git_conflict_to_conflict_detected() {
        let result = MergeResult::Conflict {
            details: Some("conflicts in server/src/foo.rs".to_string()),
            output: "merge failed".to_string(),
        };
        match result.to_merge_failure_kind() {
            MergeFailureKind::ConflictDetected(details) => {
                assert_eq!(details.as_deref(), Some("conflicts in server/src/foo.rs"));
            }
            other => panic!("expected ConflictDetected, got {other:?}"),
        }
    }

    #[test]
    fn to_failure_kind_maps_no_commits() {
        let result = MergeResult::NoCommits {
            output: "no commits".to_string(),
        };
        assert!(matches!(
            result.to_merge_failure_kind(),
            MergeFailureKind::NoCommits
        ));
    }

    #[test]
    fn to_failure_kind_maps_other() {
        let result = MergeResult::Other {
            output: "cherry-pick failed".to_string(),
            conflict_details: None,
        };
        assert!(matches!(
            result.to_merge_failure_kind(),
            MergeFailureKind::Other(_)
        ));
    }

    #[test]
    #[should_panic(expected = "to_merge_failure_kind called on MergeResult::Success")]
    fn to_failure_kind_panics_on_success() {
        let result = MergeResult::Success {
            conflicts_resolved: false,
            conflict_details: None,
            gate_output: String::new(),
        };
        let _ = result.to_merge_failure_kind();
    }
}
