//! Show the last test run

use crate::commands::utils::{open_repository, resolve_run_id};
use crate::commands::Command;
use crate::error::Result;
use crate::ui::UI;

fn display_captured_stderr(ui: &mut dyn UI, stderr: &[u8]) -> Result<()> {
    if stderr.is_empty() {
        return Ok(());
    }
    ui.output("")?;
    ui.output("Captured stderr:")?;
    ui.output_bytes(stderr)?;
    if !stderr.ends_with(b"\n") {
        ui.output("")?;
    }
    Ok(())
}

/// Command to display results from a test run.
///
/// Shows test statistics and details about failed tests from a
/// specific run or the most recent run stored in the repository.
pub struct LastCommand {
    base_path: Option<String>,
    run_id: Option<String>,
    subunit: bool,
    show_output: bool,
}

impl LastCommand {
    /// Creates a new last command with default settings.
    ///
    /// # Arguments
    /// * `base_path` - Optional base directory path for the repository
    pub fn new(base_path: Option<String>) -> Self {
        LastCommand {
            base_path,
            run_id: None,
            subunit: false,
            show_output: true,
        }
    }

    /// Creates a new last command for a specific run.
    ///
    /// # Arguments
    /// * `base_path` - Optional base directory path for the repository
    /// * `run_id` - Run ID to show (supports negative indices)
    pub fn with_run(base_path: Option<String>, run_id: Option<String>) -> Self {
        LastCommand {
            base_path,
            run_id,
            subunit: false,
            show_output: true,
        }
    }

    /// Creates a last command that outputs results in subunit format.
    ///
    /// # Arguments
    /// * `base_path` - Optional base directory path for the repository
    /// * `run_id` - Optional run ID (supports negative indices)
    pub fn with_subunit(base_path: Option<String>, run_id: Option<String>) -> Self {
        LastCommand {
            base_path,
            run_id,
            subunit: true,
            show_output: false,
        }
    }

    /// Creates a last command with control over output display.
    ///
    /// # Arguments
    /// * `base_path` - Optional base directory path for the repository
    /// * `run_id` - Optional run ID (supports negative indices)
    /// * `show_output` - Whether to show detailed output for failed tests
    pub fn with_output_control(
        base_path: Option<String>,
        run_id: Option<String>,
        show_output: bool,
    ) -> Self {
        LastCommand {
            base_path,
            run_id,
            subunit: false,
            show_output,
        }
    }
}

impl Command for LastCommand {
    fn execute(&self, ui: &mut dyn UI) -> Result<i32> {
        let repo = open_repository(self.base_path.as_deref())?;
        let run_id = resolve_run_id(&*repo, self.run_id.as_deref())?;
        let in_progress = repo.is_run_in_progress(&run_id)?;
        let test_run = repo.get_test_run(&run_id)?;

        if self.subunit {
            // Output the test run as a subunit stream
            let mut buffer = Vec::new();
            crate::subunit_stream::write_stream(&test_run, &mut buffer)?;
            ui.output_bytes(&buffer)?;
            return Ok(0); // Exit code 0 if we successfully wrote the stream
        }

        ui.output(&format!("Test run: {}", test_run.id))?;
        if in_progress {
            ui.output("Status: in progress")?;
        }
        ui.output(&format!("Timestamp: {}", test_run.timestamp))?;
        ui.output(&format!("Total tests: {}", test_run.total_tests()))?;
        ui.output(&format!("Passed: {}", test_run.count_successes()))?;
        ui.output(&format!("Failed: {}", test_run.count_failures()))?;

        // Show total duration if available
        if let Some(duration) = test_run.total_duration() {
            ui.output(&format!("Total time: {:.3}s", duration.as_secs_f64()))?;
        }

        // Show test output based on show_output setting
        if self.show_output && test_run.count_failures() > 0 {
            // Replay the raw subunit stream with output filter to show failures
            let mut raw_stream = repo.get_test_run_raw(&test_run.id)?;

            let output_filter = crate::subunit_stream::OutputFilter::FailuresOnly;

            // Parse and display output
            crate::subunit_stream::parse_stream_with_progress(
                &mut raw_stream,
                test_run.id.clone(),
                |_test_id, _status| {
                    // No progress callback needed for last command
                },
                |bytes| {
                    // Output callback - write to UI
                    let _ = ui.output_bytes(bytes);
                },
                |_result| {},
                output_filter,
            )?;

            // If there was no output (no file attachments in stream), show test IDs
            ui.output("")?;
            ui.output("Failed tests:")?;
            for test_id in test_run.get_failing_tests() {
                ui.output(&format!("  {}", test_id))?;
            }
        } else if test_run.count_failures() > 0 {
            // Just list the test IDs without details
            ui.output("")?;
            ui.output("Failed tests:")?;
            for test_id in test_run.get_failing_tests() {
                ui.output(&format!("  {}", test_id))?;
            }
        }

        if let Some(stderr) = repo.get_run_stderr(&run_id)? {
            display_captured_stderr(ui, &stderr)?;
        }

        if test_run.count_failures() > 0 {
            Ok(1)
        } else {
            Ok(0)
        }
    }

    fn name(&self) -> &str {
        "last"
    }

    fn help(&self) -> &str {
        "Show the results from the last test run"
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::repository::inquest::InquestRepositoryFactory;
    use crate::repository::{RepositoryFactory, RunId, TestId, TestResult, TestRun, TestStatus};
    use crate::ui::test_ui::TestUI;
    use tempfile::TempDir;

    #[test]
    fn test_last_command() {
        let temp = TempDir::new().unwrap();

        // Initialize repository and add a test run
        let factory = InquestRepositoryFactory;
        let mut repo = factory.initialise(temp.path()).unwrap();

        let mut test_run = TestRun::new(RunId::new("0"));
        test_run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
        test_run.add_result(TestResult {
            test_id: TestId::new("test1"),
            status: TestStatus::Success,
            duration: None,
            message: None,
            details: None,
            tags: vec![],
        });

        repo.insert_test_run(test_run).unwrap();

        // Execute last command
        let mut ui = TestUI::new();
        let cmd = LastCommand::new(Some(temp.path().to_string_lossy().to_string()));
        let result = cmd.execute(&mut ui);

        assert_eq!(result.unwrap(), 0);
        assert!(ui.output.iter().any(|s| s.contains("Test run: 0")));
        assert!(ui.output.iter().any(|s| s.contains("Total tests: 1")));
        assert!(ui.output.iter().any(|s| s.contains("Passed: 1")));
        assert!(ui.output.iter().any(|s| s.contains("Failed: 0")));
    }

    #[test]
    fn test_last_command_with_failures() {
        let temp = TempDir::new().unwrap();

        let factory = InquestRepositoryFactory;
        let mut repo = factory.initialise(temp.path()).unwrap();

        let mut test_run = TestRun::new(RunId::new("0"));
        test_run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
        test_run.add_result(TestResult {
            test_id: TestId::new("test1"),
            status: TestStatus::Success,
            duration: None,
            message: None,
            details: None,
            tags: vec![],
        });
        test_run.add_result(TestResult {
            test_id: TestId::new("test2"),
            status: TestStatus::Failure,
            duration: None,
            message: Some("Failed".to_string()),
            details: Some("Traceback (most recent call last):\n  test failure\n".to_string()),
            tags: vec![],
        });

        repo.insert_test_run(test_run).unwrap();

        let mut ui = TestUI::new();
        let cmd = LastCommand::new(Some(temp.path().to_string_lossy().to_string()));
        let result = cmd.execute(&mut ui);

        assert_eq!(result.unwrap(), 1); // Non-zero exit code for failures

        // Check the summary stats in string output
        assert_eq!(ui.output[0], "Test run: 0");
        assert!(ui.output[1].starts_with("Timestamp: "));
        assert_eq!(ui.output[2], "Total tests: 2");
        assert_eq!(ui.output[3], "Passed: 1");
        assert_eq!(ui.output[4], "Failed: 1");

        // When replaying from raw subunit stream with show_output=true,
        // the detailed output is sent via bytes_output callback.
        // Since insert_test_run() uses write_stream() which doesn't include
        // file attachments, there will be no detailed output to show.
        // The filtering only shows output for tests that have file attachments.
        assert_eq!(ui.bytes_output.len(), 0);
    }

    #[test]
    fn test_last_command_shows_captured_stderr() {
        let temp = TempDir::new().unwrap();
        let factory = InquestRepositoryFactory;
        let mut repo = factory.initialise(temp.path()).unwrap();

        let mut test_run = TestRun::new(RunId::new("0"));
        test_run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
        test_run.add_result(TestResult {
            test_id: TestId::new("test1"),
            status: TestStatus::Success,
            duration: None,
            message: None,
            details: None,
            tags: vec![],
        });
        let run_id = repo.insert_test_run(test_run).unwrap();
        repo.set_run_stderr(&run_id, b"runner crashed: oops\n")
            .unwrap();

        let mut ui = TestUI::new();
        let cmd = LastCommand::new(Some(temp.path().to_string_lossy().to_string()));
        cmd.execute(&mut ui).unwrap();

        assert!(
            ui.output.iter().any(|s| s == "Captured stderr:"),
            "expected 'Captured stderr:' header in {:?}",
            ui.output
        );
        let combined: Vec<u8> = ui.bytes_output.iter().flatten().copied().collect();
        assert_eq!(combined, b"runner crashed: oops\n");
    }

    #[test]
    fn test_last_command_shows_full_stderr_without_truncation() {
        // Larger than the previous 16 KiB cutoff. The full payload should
        // make it through unmodified and no "elided" notice should appear.
        let temp = TempDir::new().unwrap();
        let factory = InquestRepositoryFactory;
        let mut repo = factory.initialise(temp.path()).unwrap();

        let mut test_run = TestRun::new(RunId::new("0"));
        test_run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
        test_run.add_result(TestResult {
            test_id: TestId::new("test1"),
            status: TestStatus::Success,
            duration: None,
            message: None,
            details: None,
            tags: vec![],
        });
        let run_id = repo.insert_test_run(test_run).unwrap();

        let payload: Vec<u8> = (0..50_000).map(|i| b"abcdef\n"[i % 7]).collect();
        repo.set_run_stderr(&run_id, &payload).unwrap();

        let mut ui = TestUI::new();
        let cmd = LastCommand::new(Some(temp.path().to_string_lossy().to_string()));
        cmd.execute(&mut ui).unwrap();

        assert!(
            !ui.output.iter().any(|s| s.contains("elided")),
            "expected no truncation notice in {:?}",
            ui.output
        );
        let combined: Vec<u8> = ui.bytes_output.iter().flatten().copied().collect();
        assert_eq!(combined, payload);
    }

    #[test]
    fn test_last_command_skips_empty_stderr_file() {
        let temp = TempDir::new().unwrap();
        let factory = InquestRepositoryFactory;
        let mut repo = factory.initialise(temp.path()).unwrap();

        let mut test_run = TestRun::new(RunId::new("0"));
        test_run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
        test_run.add_result(TestResult {
            test_id: TestId::new("test1"),
            status: TestStatus::Success,
            duration: None,
            message: None,
            details: None,
            tags: vec![],
        });
        repo.insert_test_run(test_run).unwrap();

        let mut ui = TestUI::new();
        let cmd = LastCommand::new(Some(temp.path().to_string_lossy().to_string()));
        cmd.execute(&mut ui).unwrap();

        assert!(!ui.output.iter().any(|s| s == "Captured stderr:"));
    }
}
