//! Workflow module: test result tracking and acceptance evaluation.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Whether an individual test case passed or failed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TestStatus {
    Pass,
    Fail,
}

/// The name, status, and optional details for a single test case execution.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestCaseResult {
    pub name: String,
    pub status: TestStatus,
    pub details: Option<String>,
}

struct TestRunSummary {
    total: usize,
    failed: usize,
}

/// The outcome of evaluating whether a story is ready for acceptance.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcceptanceDecision {
    pub can_accept: bool,
    pub reasons: Vec<String>,
    pub warning: Option<String>,
}

/// Collected unit and integration test results recorded for a single story.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StoryTestResults {
    pub unit: Vec<TestCaseResult>,
    pub integration: Vec<TestCaseResult>,
}

/// Server-wide in-memory workflow state: test results and coverage reports keyed by story ID.
#[derive(Debug, Clone, Default)]
pub struct WorkflowState {
    pub results: HashMap<String, StoryTestResults>,
    pub coverage: HashMap<String, CoverageReport>,
}

impl WorkflowState {
    /// Record unit and integration test results for a story, rejecting batches with more than one failure.
    pub fn record_test_results_validated(
        &mut self,
        story_id: String,
        unit: Vec<TestCaseResult>,
        integration: Vec<TestCaseResult>,
    ) -> Result<(), String> {
        let failures = unit
            .iter()
            .chain(integration.iter())
            .filter(|test| test.status == TestStatus::Fail)
            .count();

        if failures > 1 {
            return Err(format!(
                "Multiple failing tests detected ({failures}); register failures one at a time."
            ));
        }

        self.results
            .insert(story_id, StoryTestResults { unit, integration });

        Ok(())
    }
}

fn summarize_results(results: &StoryTestResults) -> TestRunSummary {
    let mut total = 0;
    let mut failed = 0;

    for test in results.unit.iter().chain(results.integration.iter()) {
        total += 1;
        if test.status == TestStatus::Fail {
            failed += 1;
        }
    }

    TestRunSummary { total, failed }
}

fn evaluate_acceptance(results: &StoryTestResults) -> AcceptanceDecision {
    let summary = summarize_results(results);

    if summary.failed == 0 && summary.total > 0 {
        return AcceptanceDecision {
            can_accept: true,
            reasons: Vec::new(),
            warning: None,
        };
    }

    let mut reasons = Vec::new();
    if summary.total == 0 {
        reasons.push("No test results recorded for the story.".to_string());
    }
    if summary.failed > 0 {
        reasons.push(format!(
            "{} test(s) are failing; acceptance is blocked.",
            summary.failed
        ));
    }

    let warning = if summary.failed > 1 {
        Some(format!(
            "Multiple tests are failing ({} failures).",
            summary.failed
        ))
    } else {
        None
    };

    AcceptanceDecision {
        can_accept: false,
        reasons,
        warning,
    }
}

/// Coverage report for a story.
#[derive(Debug, Clone, PartialEq)]
pub struct CoverageReport {
    pub current_percent: f64,
    pub threshold_percent: f64,
    pub baseline_percent: Option<f64>,
}

/// Evaluate acceptance with optional coverage data.
pub fn evaluate_acceptance_with_coverage(
    results: &StoryTestResults,
    coverage: Option<&CoverageReport>,
) -> AcceptanceDecision {
    let mut decision = evaluate_acceptance(results);

    if let Some(report) = coverage {
        if report.current_percent < report.threshold_percent {
            decision.can_accept = false;
            decision.reasons.push(format!(
                "Coverage below threshold ({:.1}% < {:.1}%).",
                report.current_percent, report.threshold_percent
            ));
        }
        if let Some(baseline) = report.baseline_percent
            && report.current_percent < baseline
        {
            decision.can_accept = false;
            decision.reasons.push(format!(
                "Coverage regression: {:.1}% → {:.1}% (threshold: {:.1}%).",
                baseline, report.current_percent, report.threshold_percent
            ));
        }
    }

    decision
}

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

    // === evaluate_acceptance_with_coverage ===

    #[test]
    fn acceptance_blocked_by_coverage_below_threshold() {
        let results = StoryTestResults {
            unit: vec![TestCaseResult {
                name: "unit-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
            integration: vec![TestCaseResult {
                name: "int-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
        };

        let coverage = CoverageReport {
            current_percent: 55.0,
            threshold_percent: 80.0,
            baseline_percent: None,
        };

        let decision = evaluate_acceptance_with_coverage(&results, Some(&coverage));
        assert!(!decision.can_accept);
        assert!(
            decision
                .reasons
                .iter()
                .any(|r| r.contains("Coverage below threshold"))
        );
    }

    #[test]
    fn acceptance_blocked_by_coverage_regression() {
        let results = StoryTestResults {
            unit: vec![TestCaseResult {
                name: "unit-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
            integration: vec![TestCaseResult {
                name: "int-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
        };

        let coverage = CoverageReport {
            current_percent: 82.0,
            threshold_percent: 80.0,
            baseline_percent: Some(90.0),
        };

        let decision = evaluate_acceptance_with_coverage(&results, Some(&coverage));
        assert!(!decision.can_accept);
        assert!(
            decision
                .reasons
                .iter()
                .any(|r| r.contains("Coverage regression"))
        );
    }

    #[test]
    fn acceptance_passes_with_good_coverage() {
        let results = StoryTestResults {
            unit: vec![TestCaseResult {
                name: "unit-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
            integration: vec![TestCaseResult {
                name: "int-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
        };

        let coverage = CoverageReport {
            current_percent: 92.0,
            threshold_percent: 80.0,
            baseline_percent: Some(90.0),
        };

        let decision = evaluate_acceptance_with_coverage(&results, Some(&coverage));
        assert!(decision.can_accept);
    }

    #[test]
    fn acceptance_works_without_coverage_data() {
        let results = StoryTestResults {
            unit: vec![TestCaseResult {
                name: "unit-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
            integration: vec![TestCaseResult {
                name: "int-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
        };

        let decision = evaluate_acceptance_with_coverage(&results, None);
        assert!(decision.can_accept);
    }

    // === evaluate_acceptance ===

    #[test]
    fn warns_when_multiple_tests_fail() {
        let results = StoryTestResults {
            unit: vec![
                TestCaseResult {
                    name: "unit-1".to_string(),
                    status: TestStatus::Fail,
                    details: None,
                },
                TestCaseResult {
                    name: "unit-2".to_string(),
                    status: TestStatus::Fail,
                    details: None,
                },
            ],
            integration: vec![TestCaseResult {
                name: "integration-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
        };

        let decision = evaluate_acceptance(&results);

        assert!(!decision.can_accept);
        assert_eq!(
            decision.warning,
            Some("Multiple tests are failing (2 failures).".to_string())
        );
    }

    #[test]
    fn accepts_when_all_tests_pass() {
        let results = StoryTestResults {
            unit: vec![TestCaseResult {
                name: "unit-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
            integration: vec![TestCaseResult {
                name: "integration-1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
        };

        let decision = evaluate_acceptance(&results);
        assert!(decision.can_accept);
        assert!(decision.reasons.is_empty());
        assert!(decision.warning.is_none());
    }

    #[test]
    fn rejects_when_no_results_recorded() {
        let results = StoryTestResults::default();
        let decision = evaluate_acceptance(&results);
        assert!(!decision.can_accept);
        assert!(
            decision
                .reasons
                .iter()
                .any(|r| r.contains("No test results"))
        );
    }

    #[test]
    fn rejects_with_single_failure_no_warning() {
        let results = StoryTestResults {
            unit: vec![
                TestCaseResult {
                    name: "unit-1".to_string(),
                    status: TestStatus::Pass,
                    details: None,
                },
                TestCaseResult {
                    name: "unit-2".to_string(),
                    status: TestStatus::Fail,
                    details: None,
                },
            ],
            integration: vec![],
        };

        let decision = evaluate_acceptance(&results);
        assert!(!decision.can_accept);
        assert!(decision.reasons.iter().any(|r| r.contains("failing")));
        assert!(decision.warning.is_none());
    }

    // === record_test_results_validated ===

    #[test]
    fn rejects_recording_multiple_failures() {
        let mut state = WorkflowState::default();
        let unit = vec![
            TestCaseResult {
                name: "unit-1".to_string(),
                status: TestStatus::Fail,
                details: None,
            },
            TestCaseResult {
                name: "unit-2".to_string(),
                status: TestStatus::Fail,
                details: None,
            },
        ];
        let integration = vec![TestCaseResult {
            name: "integration-1".to_string(),
            status: TestStatus::Pass,
            details: None,
        }];

        let result = state.record_test_results_validated("story-26".to_string(), unit, integration);

        assert!(result.is_err());
    }

    #[test]
    fn record_valid_results_stores_them() {
        let mut state = WorkflowState::default();
        let unit = vec![TestCaseResult {
            name: "unit-1".to_string(),
            status: TestStatus::Pass,
            details: None,
        }];
        let integration = vec![TestCaseResult {
            name: "int-1".to_string(),
            status: TestStatus::Pass,
            details: None,
        }];

        let result = state.record_test_results_validated("story-29".to_string(), unit, integration);
        assert!(result.is_ok());
        assert!(state.results.contains_key("story-29"));
        assert_eq!(state.results["story-29"].unit.len(), 1);
        assert_eq!(state.results["story-29"].integration.len(), 1);
    }
}
