//! Shared acceptance operations for CLI and TUI modes.
//!
//! Provides acceptance test execution after apply and before archive.

#![allow(dead_code)]

use crate::agent::AgentRunner;
use crate::error::{OrchestratorError, Result};
use crate::history::{AcceptanceAttempt, OutputCollector};
use crate::openspec::Change;
use tracing::{info, warn};

use super::output::OutputHandler;

const ACCEPTANCE_OUTPUT_FALLBACK: &str = "No acceptance output captured";

pub fn build_acceptance_tail_findings(
    stdout_tail: Option<String>,
    stderr_tail: Option<String>,
) -> Vec<String> {
    let stdout = stdout_tail.filter(|text| !text.trim().is_empty());
    let stderr = stderr_tail.filter(|text| !text.trim().is_empty());
    let selected = stdout
        .or(stderr)
        .unwrap_or_else(|| ACCEPTANCE_OUTPUT_FALLBACK.to_string());
    let lines = selected
        .lines()
        .filter(|line| {
            let trimmed = line.trim();
            // Filter out empty lines, ACCEPTANCE: markers, and FINDINGS: lines
            !trimmed.is_empty()
                && !trimmed.starts_with("ACCEPTANCE:")
                && !trimmed.starts_with("FINDINGS:")
        })
        .map(|line| line.to_string())
        .collect::<Vec<_>>();
    if lines.is_empty() {
        vec![ACCEPTANCE_OUTPUT_FALLBACK.to_string()]
    } else {
        lines
    }
}

/// Result of an acceptance operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AcceptanceResult {
    /// Acceptance passed - can proceed to archive.
    Pass,
    /// Acceptance failed - must return to apply loop.
    Fail { findings: Vec<String> },
    /// Acceptance requires more investigation - retry acceptance.
    Continue,
    /// Acceptance blocked due to implementation blocker - stop apply loop.
    Blocked,
    /// Acceptance command execution failed (non-zero exit).
    CommandFailed {
        error: String,
        findings: Vec<String>,
    },
    /// Acceptance was cancelled (e.g., by user or timeout).
    Cancelled,
}

impl AcceptanceResult {
    /// Returns true if acceptance passed.
    pub fn is_pass(&self) -> bool {
        matches!(self, AcceptanceResult::Pass)
    }
}

/// Run acceptance test for a change with streaming output.
///
/// # Arguments
/// * `change` - The change to test
/// * `agent` - The agent runner for history tracking
/// * `ai_runner` - The AI command runner for command execution
/// * `config` - Orchestrator configuration
/// * `output` - Output handler for streaming command output
/// * `cancel_check` - Function to check if operation should be cancelled
///
/// # Returns
/// * `Ok((AcceptanceResult::Pass, attempt_number))` - Acceptance passed
/// * `Ok((AcceptanceResult::Fail { findings }, attempt_number))` - Acceptance failed with findings
/// * `Ok((AcceptanceResult::CommandFailed { error, findings }, attempt_number))` - Command execution failed
/// * `Ok((AcceptanceResult::Cancelled, attempt_number))` - Operation was cancelled
/// * `Err(e)` - An error occurred
///
/// The attempt_number is the number of the acceptance attempt that was just recorded.
pub async fn acceptance_test_streaming<O, F>(
    change: &Change,
    agent: &mut AgentRunner,
    ai_runner: &crate::ai_command_runner::AiCommandRunner,
    _config: &crate::config::OrchestratorConfig,
    output: &O,
    cancel_check: F,
) -> Result<(AcceptanceResult, u32, String)>
where
    O: OutputHandler,
    F: Fn() -> bool,
{
    use crate::agent::OutputLine;

    info!("Running acceptance test for: {}", change.id);
    output.on_info(&format!("Acceptance test: {}", change.id));

    // Capture current commit hash for diff tracking
    let commit_hash = crate::vcs::git::commands::get_current_commit(".")
        .await
        .ok(); // Allow to fail silently (non-git repos)

    // Get current branch for diff context (first acceptance needs base branch)
    let base_branch = crate::vcs::git::commands::get_current_branch(".")
        .await
        .ok()
        .flatten(); // None if in detached HEAD or non-git repo

    // Execute acceptance command with streaming via AiCommandRunner (real process handle)
    let (mut child, mut output_rx, start_time, command) = agent
        .run_acceptance_streaming_with_runner(&change.id, ai_runner, None, base_branch.as_deref())
        .await?;

    // Log acceptance started with command
    output.on_info(&format!("Acceptance started: {}", change.id));
    output.on_info(&format!("  Command: {}", command));

    // Create output collector for history and parsing
    let mut output_collector = OutputCollector::new();
    let mut full_stdout = String::new();

    // Grace period after detecting an acceptance marker before terminating the process.
    // This handles the case where the agent process (e.g., opencode) does not exit
    // promptly after emitting ACCEPTANCE: PASS/FAIL/etc., for example because
    // child processes (MCP servers) keep stdout/stderr pipes open.
    const MARKER_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(30);

    // Stream output until channel closes or acceptance marker detected + grace period
    let mut marker_detected = false;
    let mut marker_deadline: Option<tokio::time::Instant> = None;
    let mut early_terminated = false;

    loop {
        let recv_future = output_rx.recv();

        let line = if let Some(deadline) = marker_deadline {
            // After marker detection, apply a timeout for remaining output
            match tokio::time::timeout_at(deadline, recv_future).await {
                Ok(Some(line)) => line,
                Ok(None) => break, // Channel closed normally
                Err(_) => {
                    // Grace period expired — terminate the process
                    warn!(
                        "Acceptance marker grace period expired for {}, terminating process",
                        change.id
                    );
                    let _ = child.terminate();
                    early_terminated = true;
                    break;
                }
            }
        } else {
            match recv_future.await {
                Some(line) => line,
                None => break, // Channel closed normally
            }
        };

        // Check for cancellation
        if cancel_check() {
            warn!("Acceptance test cancelled for: {}", change.id);
            output.on_warn("Acceptance test cancelled");
            let _ = child.terminate();
            // Note: For cancellation, we don't record an attempt, so return 0
            return Ok((AcceptanceResult::Cancelled, 0, command));
        }

        match line {
            OutputLine::Stdout(s) => {
                output_collector.add_stdout(&s);
                full_stdout.push_str(&s);
                full_stdout.push('\n');
                output.on_stdout(&s);

                // Detect a canonical verdict in stdout to start the grace
                // period. This prevents indefinite blocking when the agent
                // process does not exit after emitting the verdict. The
                // detector recognises the primary strict JSON verdict (as a
                // standalone line or wrapped in an opencode `--format json`
                // event) and, as fallback, the legacy standalone plain-text
                // marker. Malformed markers with trailing text (for example
                // "ACCEPTANCE: PASSAll ...") do NOT trigger early completion.
                if !marker_detected && crate::acceptance::detect_verdict_in_line(&s).is_some() {
                    marker_detected = true;
                    marker_deadline = Some(tokio::time::Instant::now() + MARKER_GRACE_PERIOD);
                    info!(
                        "Acceptance canonical verdict detected for {}, starting {}s grace period",
                        change.id,
                        MARKER_GRACE_PERIOD.as_secs()
                    );
                }
            }
            OutputLine::Stderr(s) => {
                output_collector.add_stderr(&s);
                output.on_agent_stderr(&s);
            }
        }
    }

    // Child has exited, wait for status
    let status = child.wait().await.map_err(|e| {
        OrchestratorError::AgentCommand(format!(
            "Failed to wait for acceptance command for change '{}': {}",
            change.id, e
        ))
    })?;

    // Record attempt
    let stdout_tail = output_collector.stdout_tail();
    let stderr_tail = output_collector.stderr_tail();

    // Build tail findings for history recording (last N lines, used in AcceptanceAttempt).
    let tail_findings = build_acceptance_tail_findings(stdout_tail.clone(), stderr_tail.clone());

    // A verdict-finalized run is one we terminated after observing the
    // canonical standalone verdict. The non-zero exit from termination is
    // expected — the verdict drives the final result.
    let verdict_finalized_run = early_terminated && marker_detected;

    // Check if command failed (skip when verdict already finalized).
    if !status.success() && !verdict_finalized_run {
        let error_msg = format!(
            "Acceptance command failed with exit code: {:?}",
            status.code()
        );
        let attempt_number = agent.next_acceptance_attempt_number(&change.id);
        let attempt = AcceptanceAttempt {
            attempt: attempt_number,
            passed: false,
            duration: start_time.elapsed(),
            findings: Some(tail_findings.clone()),
            exit_code: status.code(),
            stdout_tail,
            stderr_tail,
            commit_hash: commit_hash.clone(),
        };
        agent.record_acceptance_attempt(&change.id, attempt);
        output.on_error(&error_msg);
        return Ok((
            AcceptanceResult::CommandFailed {
                error: error_msg,
                findings: tail_findings,
            },
            attempt_number,
            command,
        ));
    }

    // Parse acceptance output to determine result
    let parsed_result = crate::acceptance::parse_acceptance_output(&full_stdout);

    let (result, passed) = match parsed_result {
        crate::acceptance::AcceptanceResult::Pass => {
            info!("Acceptance test passed for: {}", change.id);
            output.on_info("Acceptance test: PASS");
            (AcceptanceResult::Pass, true)
        }
        crate::acceptance::AcceptanceResult::Fail {
            findings: parsed_findings,
        } => {
            info!("Acceptance test failed for: {}", change.id);
            output.on_warn("Acceptance test: FAIL");
            // Use findings parsed from the FINDINGS section of the output so that
            // the verdict and its findings share a single data source (full_stdout).
            (
                AcceptanceResult::Fail {
                    findings: parsed_findings,
                },
                false,
            )
        }
        crate::acceptance::AcceptanceResult::Continue => {
            info!("Acceptance requires continuation for: {}", change.id);
            output.on_info("Acceptance test: CONTINUE");
            (AcceptanceResult::Continue, false)
        }
        crate::acceptance::AcceptanceResult::Blocked => {
            info!("Acceptance blocked for: {}", change.id);
            output.on_warn("Acceptance test: BLOCKED");
            (AcceptanceResult::Blocked, false)
        }
    };

    let attempt_number = agent.next_acceptance_attempt_number(&change.id);
    let attempt = AcceptanceAttempt {
        attempt: attempt_number,
        passed,
        duration: start_time.elapsed(),
        findings: Some(tail_findings.clone()),
        exit_code: status.code(),
        stdout_tail,
        stderr_tail,
        commit_hash: commit_hash.clone(),
    };
    agent.record_acceptance_attempt(&change.id, attempt);
    Ok((result, attempt_number, command))
}

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

    #[test]
    fn test_build_acceptance_tail_findings_prefers_stdout() {
        let findings = build_acceptance_tail_findings(
            Some("stdout line 1\nstdout line 2".to_string()),
            Some("stderr line".to_string()),
        );

        assert_eq!(findings, vec!["stdout line 1", "stdout line 2"]);
    }

    #[test]
    fn test_build_acceptance_tail_findings_falls_back_to_stderr() {
        let findings =
            build_acceptance_tail_findings(Some("  ".to_string()), Some("stderr".to_string()));

        assert_eq!(findings, vec!["stderr"]);
    }

    #[test]
    fn test_build_acceptance_tail_findings_fallback_message() {
        let findings = build_acceptance_tail_findings(None, Some("\n\n".to_string()));

        assert_eq!(findings, vec!["No acceptance output captured"]);
    }

    #[test]
    fn test_acceptance_result_is_pass() {
        assert!(AcceptanceResult::Pass.is_pass());
        assert!(!AcceptanceResult::Fail {
            findings: vec!["error".to_string()]
        }
        .is_pass());
        assert!(!AcceptanceResult::CommandFailed {
            error: "test".to_string(),
            findings: vec!["failure".to_string()],
        }
        .is_pass());
        assert!(!AcceptanceResult::Cancelled.is_pass());
        assert!(!AcceptanceResult::Blocked.is_pass());
    }

    #[test]
    fn test_build_acceptance_tail_findings_filters_acceptance_marker() {
        let findings = build_acceptance_tail_findings(
            Some("line 1\nACCEPTANCE: FAIL\nline 2".to_string()),
            None,
        );

        assert_eq!(findings, vec!["line 1", "line 2"]);
    }

    #[test]
    fn test_build_acceptance_tail_findings_filters_findings_line() {
        let findings = build_acceptance_tail_findings(
            Some("error 1\nFINDINGS:\n- item 1\n- item 2".to_string()),
            None,
        );

        assert_eq!(findings, vec!["error 1", "- item 1", "- item 2"]);
    }

    #[test]
    fn test_build_acceptance_tail_findings_filters_both_markers() {
        let findings = build_acceptance_tail_findings(
            Some("ACCEPTANCE: FAIL\nFINDINGS:\nactual error\nanother line".to_string()),
            None,
        );

        assert_eq!(findings, vec!["actual error", "another line"]);
    }

    // Characterization tests: document the difference between tail_findings
    // and parse_acceptance_output findings so the refactor is clearly motivated.

    #[test]
    fn test_tail_findings_includes_preamble_parse_does_not() {
        // tail_findings includes all non-marker lines (preamble, postamble, items).
        // parse_acceptance_output findings includes only FINDINGS section items.
        // The refactor unifies the FAIL path to use parse_acceptance_output findings.
        let stdout = "preamble\nACCEPTANCE: FAIL\nFINDINGS:\n- Finding 1\n- Finding 2\npostamble"
            .to_string();

        let tail = build_acceptance_tail_findings(Some(stdout.clone()), None);
        // tail includes preamble, finding items, postamble
        assert!(tail.iter().any(|l| l.contains("preamble")));
        assert!(tail.iter().any(|l| l.contains("postamble")));
        assert!(tail.iter().any(|l| l.contains("Finding 1")));

        // parse_acceptance_output returns only the FINDINGS section items
        match crate::acceptance::parse_acceptance_output(&stdout) {
            crate::acceptance::AcceptanceResult::Fail { findings } => {
                assert_eq!(findings, vec!["Finding 1", "Finding 2"]);
                assert!(!findings.iter().any(|f| f.contains("preamble")));
                assert!(!findings.iter().any(|f| f.contains("postamble")));
            }
            _ => panic!("Expected Fail"),
        }
    }

    #[test]
    fn test_parse_findings_is_preferred_source_for_fail_result() {
        // After the refactor: for AcceptanceResult::Fail, findings come from
        // parse_acceptance_output (FINDINGS section), not from build_acceptance_tail_findings.
        let stdout =
            "ACCEPTANCE: FAIL\nFINDINGS:\n- src/foo.rs:10 issue A\n- src/bar.rs:5 issue B\n"
                .to_string();

        match crate::acceptance::parse_acceptance_output(&stdout) {
            crate::acceptance::AcceptanceResult::Fail { findings } => {
                assert_eq!(findings.len(), 2);
                assert_eq!(findings[0], "src/foo.rs:10 issue A");
                assert_eq!(findings[1], "src/bar.rs:5 issue B");
            }
            _ => panic!("Expected Fail"),
        }
    }
}
