//! Test result and status tracking
//!
//! Defines test outcomes, status tracking, and result reporting.

use crate::golden::GoldenMismatch;
use crate::platform::Platform;
use chrono::{DateTime, Utc};
use std::path::PathBuf;
use std::time::Duration;
use uuid::Uuid;

/// Test execution status
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum TestStatus {
    /// Test passed
    Passed,
    /// Test failed
    Failed,
    /// Test was skipped
    Skipped,
    /// Test execution timed out
    TimedOut,
}

impl std::fmt::Display for TestStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TestStatus::Passed => write!(f, "PASSED"),
            TestStatus::Failed => write!(f, "FAILED"),
            TestStatus::Skipped => write!(f, "SKIPPED"),
            TestStatus::TimedOut => write!(f, "TIMED_OUT"),
        }
    }
}

/// A single test execution instance
#[derive(Debug, Clone)]
pub struct TestExecution {
    /// Unique execution ID
    pub id: Uuid,
    /// Fixture name being tested
    pub fixture: String,
    /// Target platform
    pub platform: Platform,
    /// Execution start time
    pub started_at: DateTime<Utc>,
    /// Execution end time
    pub ended_at: Option<DateTime<Utc>>,
    /// Docker container ID (if using container)
    pub container_id: Option<String>,
}

impl TestExecution {
    /// Create a new test execution
    pub fn new(fixture: &str, platform: Platform) -> Self {
        TestExecution {
            id: Uuid::new_v4(),
            fixture: fixture.to_string(),
            platform,
            started_at: Utc::now(),
            ended_at: None,
            container_id: None,
        }
    }

    /// Set the end time
    pub fn finish(&mut self) {
        self.ended_at = Some(Utc::now());
    }

    /// Get the execution duration
    pub fn duration(&self) -> Option<Duration> {
        self.ended_at.map(|end| {
            let duration = end.signed_duration_since(self.started_at);
            Duration::from_secs(duration.num_seconds() as u64)
        })
    }

    /// Set container ID
    pub fn with_container_id(mut self, id: String) -> Self {
        self.container_id = Some(id);
        self
    }
}

/// Complete test result with outcome
#[derive(Debug)]
pub struct TestResult {
    /// Execution metadata
    pub execution: TestExecution,
    /// Test status/outcome
    pub status: TestStatus,
    /// Files generated by ggen sync
    pub generated_files: Vec<PathBuf>,
    /// Golden file mismatches (if any)
    pub mismatches: Vec<GoldenMismatch>,
    /// Captured stdout/stderr
    pub logs: String,
    /// Error message (if failed)
    pub error: Option<String>,
}

impl TestResult {
    /// Create a passing test result
    pub fn passed(execution: TestExecution, files: Vec<PathBuf>) -> Self {
        TestResult {
            execution,
            status: TestStatus::Passed,
            generated_files: files,
            mismatches: Vec::new(),
            logs: String::new(),
            error: None,
        }
    }

    /// Create a failing test result
    pub fn failed(execution: TestExecution, error: String, logs: String) -> Self {
        TestResult {
            execution,
            status: TestStatus::Failed,
            generated_files: Vec::new(),
            mismatches: Vec::new(),
            logs,
            error: Some(error),
        }
    }

    /// Create a timed out test result
    pub fn timed_out(execution: TestExecution, timeout: Duration, logs: String) -> Self {
        TestResult {
            execution,
            status: TestStatus::TimedOut,
            generated_files: Vec::new(),
            mismatches: Vec::new(),
            logs,
            error: Some(format!("Test timed out after {:?}", timeout)),
        }
    }

    /// Create a skipped test result
    pub fn skipped(execution: TestExecution, reason: String) -> Self {
        TestResult {
            execution,
            status: TestStatus::Skipped,
            generated_files: Vec::new(),
            mismatches: Vec::new(),
            logs: reason.clone(),
            error: Some(reason),
        }
    }

    /// Add a golden file mismatch
    pub fn add_mismatch(&mut self, mismatch: GoldenMismatch) {
        self.mismatches.push(mismatch);
        self.status = TestStatus::Failed;
    }

    /// Check if test passed
    pub fn is_success(&self) -> bool {
        self.status == TestStatus::Passed
    }

    /// Get summary string
    pub fn summary(&self) -> String {
        format!(
            "[{}] {} on {} ({})",
            self.status,
            self.execution.fixture,
            self.execution.platform,
            self.execution
                .duration()
                .map(|d| format!("{:?}", d))
                .unwrap_or_else(|| "in progress".to_string())
        )
    }

    /// Generate JUnit XML representation
    pub fn to_junit_xml(&self) -> String {
        let duration_secs = self
            .execution
            .duration()
            .map(|d| d.as_secs_f64())
            .unwrap_or(0.0);

        let status_str = match self.status {
            TestStatus::Passed => "passed",
            TestStatus::Failed => "failed",
            TestStatus::Skipped => "skipped",
            TestStatus::TimedOut => "error",
        };

        let mut xml = format!(
            r#"<testcase name="{}" classname="ggen_e2e.{}" time="{}" status="{}">"#,
            self.execution.fixture, self.execution.platform, duration_secs, status_str
        );

        if let Some(error_msg) = &self.error {
            xml.push_str(&format!(
                r#"<failure message="{}">{}</failure>"#,
                error_msg, error_msg
            ));
        }

        if !self.logs.is_empty() {
            xml.push_str(&format!(
                r#"<system-out>{}</system-out>"#,
                escape_xml(&self.logs)
            ));
        }

        for mismatch in &self.mismatches {
            xml.push_str(&format!(
                r#"<failure type="golden_mismatch" message="{}{}">
{}</failure>"#,
                mismatch.file.display(),
                "",
                escape_xml(&mismatch.diff)
            ));
        }

        xml.push_str("</testcase>");
        xml
    }
}

impl std::fmt::Display for TestResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.summary())
    }
}

/// Escape XML special characters
fn escape_xml(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&apos;")
}

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

    #[test]
    fn test_test_status_display() {
        assert_eq!(TestStatus::Passed.to_string(), "PASSED");
        assert_eq!(TestStatus::Failed.to_string(), "FAILED");
        assert_eq!(TestStatus::Skipped.to_string(), "SKIPPED");
        assert_eq!(TestStatus::TimedOut.to_string(), "TIMED_OUT");
    }

    #[test]
    fn test_execution_creation() {
        let platform = Platform {
            name: "test".to_string(),
            os: crate::platform::Os::Linux,
            arch: crate::platform::Arch::X86_64,
            docker_available: true,
        };
        let exec = TestExecution::new("test_fixture", platform);

        assert_eq!(exec.fixture, "test_fixture");
        assert!(exec.ended_at.is_none());
        assert!(exec.container_id.is_none());
    }

    #[test]
    fn test_execution_duration() {
        let platform = Platform {
            name: "test".to_string(),
            os: crate::platform::Os::Linux,
            arch: crate::platform::Arch::X86_64,
            docker_available: true,
        };
        let mut exec = TestExecution::new("test", platform);
        assert!(exec.duration().is_none());

        exec.finish();
        assert!(exec.duration().is_some());
    }

    #[test]
    fn test_result_passed() {
        let platform = Platform {
            name: "test".to_string(),
            os: crate::platform::Os::Linux,
            arch: crate::platform::Arch::X86_64,
            docker_available: true,
        };
        let exec = TestExecution::new("test", platform);
        let files = vec![PathBuf::from("output.txt")];
        let result = TestResult::passed(exec, files);

        assert_eq!(result.status, TestStatus::Passed);
        assert!(result.is_success());
        assert_eq!(result.generated_files.len(), 1);
    }

    #[test]
    fn test_result_failed() {
        let platform = Platform {
            name: "test".to_string(),
            os: crate::platform::Os::Linux,
            arch: crate::platform::Arch::X86_64,
            docker_available: true,
        };
        let exec = TestExecution::new("test", platform);
        let result = TestResult::failed(exec, "error".to_string(), "logs".to_string());

        assert_eq!(result.status, TestStatus::Failed);
        assert!(!result.is_success());
    }

    #[test]
    fn test_escape_xml() {
        assert_eq!(escape_xml("a&b"), "a&amp;b");
        assert_eq!(escape_xml("a<b>c"), "a&lt;b&gt;c");
        assert_eq!(escape_xml(r#"a"b'c"#), "a&quot;b&apos;c");
    }

    #[test]
    fn test_to_junit_xml() {
        let platform = Platform {
            name: "test".to_string(),
            os: crate::platform::Os::Linux,
            arch: crate::platform::Arch::X86_64,
            docker_available: true,
        };
        let exec = TestExecution::new("test_case", platform);
        let result = TestResult::passed(exec, vec![]);

        let xml = result.to_junit_xml();
        assert!(xml.contains("test_case"));
        assert!(xml.contains("testcase"));
    }
}
