//! Integration tests for full workflows
//!
//! These tests exercise complete user workflows by running actual commands
//! against real repositories in temporary directories.

use inquest::commands::{
    AnalyzeIsolationCommand, Command, FailingCommand, InitCommand, LastCommand, StatsCommand,
};
use inquest::repository::{RepositoryFactory, TestResult, TestRun};
use inquest::ui::UI;
use std::fs;
use std::io::Write;
use tempfile::TempDir;

/// Simple test UI that captures output for assertions
struct TestUI {
    output: Vec<String>,
    errors: Vec<String>,
    bytes_output: Vec<Vec<u8>>,
}

impl TestUI {
    fn new() -> Self {
        TestUI {
            output: Vec::new(),
            errors: Vec::new(),
            bytes_output: Vec::new(),
        }
    }
}

impl UI for TestUI {
    fn output(&mut self, message: &str) -> inquest::error::Result<()> {
        self.output.push(message.to_string());
        Ok(())
    }

    fn error(&mut self, message: &str) -> inquest::error::Result<()> {
        self.errors.push(message.to_string());
        Ok(())
    }

    fn warning(&mut self, message: &str) -> inquest::error::Result<()> {
        self.errors.push(format!("Warning: {}", message));
        Ok(())
    }

    fn output_bytes(&mut self, bytes: &[u8]) -> inquest::error::Result<()> {
        self.bytes_output.push(bytes.to_vec());
        Ok(())
    }
}

#[test]
fn test_full_workflow_init_load_last() {
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Step 1: Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    let result = init_cmd.execute(&mut ui);
    assert_eq!(result.unwrap(), 0);
    assert!(ui.output[0].contains("Initialized"));

    // Verify repository was created
    assert!(temp.path().join(".inquest").exists());
    assert!(temp.path().join(".inquest/format").exists());

    // Step 2: Load a test run
    let mut test_run = TestRun::new("0".to_string());
    test_run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
    test_run.add_result(TestResult::success("test1"));
    test_run.add_result(TestResult::failure("test2", "Failed"));
    test_run.add_result(TestResult::success("test3"));

    // Load the test run directly using the repository API
    // (In real usage, this would be done via LoadCommand reading from stdin)
    let factory = inquest::repository::inquest::InquestRepositoryFactory;
    let mut repo = factory.open(temp.path()).unwrap();
    repo.insert_test_run(test_run).unwrap();

    // Step 3: Check stats
    let mut ui = TestUI::new();
    let stats_cmd = StatsCommand::new(Some(base_path.clone()));
    let result = stats_cmd.execute(&mut ui);
    assert_eq!(result.unwrap(), 0);
    assert_eq!(ui.output.len(), 6);
    assert_eq!(ui.output[0], "Repository Statistics:");
    assert_eq!(ui.output[1], "  Total test runs: 1");
    assert_eq!(ui.output[2], "  Latest run: 0");
    assert_eq!(ui.output[3], "  Tests in latest run: 3");
    assert_eq!(ui.output[4], "  Failures in latest run: 1");
    assert_eq!(ui.output[5], "  Total tests executed: 3");

    // Step 4: Get last run
    let mut ui = TestUI::new();
    let last_cmd = LastCommand::new(Some(base_path.clone()));
    let result = last_cmd.execute(&mut ui);
    assert_eq!(result.unwrap(), 1); // Exit code 1 because there's a failure

    // Verify exact output structure
    // Note: insert_test_run() doesn't include file attachments, so we only get test IDs
    assert_eq!(ui.output.len(), 8);
    assert_eq!(ui.output[0], "Test run: 0");
    assert!(ui.output[1].starts_with("Timestamp: "));
    assert_eq!(ui.output[2], "Total tests: 3");
    assert_eq!(ui.output[3], "Passed: 2");
    assert_eq!(ui.output[4], "Failed: 1");
    assert_eq!(ui.output[5], "");
    assert_eq!(ui.output[6], "Failed tests:");
    assert_eq!(ui.output[7], "  test2");

    // No detailed output since insert_test_run() doesn't write file attachments
    assert_eq!(ui.bytes_output.len(), 0);
}

#[test]
fn test_workflow_with_failing_tests() {
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    init_cmd.execute(&mut ui).unwrap();

    // Load first run with failures
    let mut run1 = TestRun::new("0".to_string());
    run1.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
    run1.add_result(TestResult::success("test1"));
    run1.add_result(TestResult::failure("test2", "Error"));
    run1.add_result(TestResult::failure("test3", "Error"));

    let factory = inquest::repository::inquest::InquestRepositoryFactory;
    let mut repo = factory.open(temp.path()).unwrap();
    repo.insert_test_run_partial(run1, false).unwrap();

    // Check failing tests
    let mut ui = TestUI::new();
    let failing_cmd = FailingCommand::new(Some(base_path.clone()));
    let result = failing_cmd.execute(&mut ui);
    assert_eq!(result.unwrap(), 1); // Exit code 1 when there are failures
    assert_eq!(ui.output[0], "2 failing test(s):");
    // The order might vary, so check both test IDs are present
    assert!(ui.output[1] == "  test2" || ui.output[1] == "  test3");
    assert!(ui.output[2] == "  test2" || ui.output[2] == "  test3");
    assert_ne!(ui.output[1], ui.output[2]); // Make sure they're different

    // Load second run where test2 passes
    let mut run2 = TestRun::new("1".to_string());
    run2.timestamp = chrono::DateTime::from_timestamp(1000000001, 0).unwrap();
    run2.add_result(TestResult::success("test1"));
    run2.add_result(TestResult::success("test2"));
    run2.add_result(TestResult::failure("test3", "Still failing"));

    repo.insert_test_run_partial(run2, false).unwrap();

    // Check failing tests again - should only have test3
    let mut ui = TestUI::new();
    let failing_cmd = FailingCommand::new(Some(base_path));
    let result = failing_cmd.execute(&mut ui);
    assert_eq!(result.unwrap(), 1); // Exit code 1 when there are failures
    assert_eq!(ui.output.len(), 2);
    assert_eq!(ui.output[0], "1 failing test(s):");
    assert_eq!(ui.output[1], "  test3");
}

#[test]
fn test_workflow_partial_mode() {
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    init_cmd.execute(&mut ui).unwrap();

    let factory = inquest::repository::inquest::InquestRepositoryFactory;
    let mut repo = factory.open(temp.path()).unwrap();

    // First full run
    let mut run1 = TestRun::new("0".to_string());
    run1.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
    run1.add_result(TestResult::failure("test1", "Error"));
    run1.add_result(TestResult::failure("test2", "Error"));
    run1.add_result(TestResult::success("test3"));

    repo.insert_test_run_partial(run1, false).unwrap();

    // Check we have 2 failing tests
    let failing = repo.get_failing_tests().unwrap();
    assert_eq!(failing.len(), 2);

    // Second partial run - only test test1
    let mut run2 = TestRun::new("1".to_string());
    run2.timestamp = chrono::DateTime::from_timestamp(1000000001, 0).unwrap();
    run2.add_result(TestResult::success("test1")); // Now passes

    repo.insert_test_run_partial(run2, true).unwrap(); // Partial mode

    // Should only have test2 failing now
    let failing = repo.get_failing_tests().unwrap();
    assert_eq!(failing.len(), 1);
    assert!(failing.iter().any(|id| id.as_str() == "test2"));
    assert!(!failing.iter().any(|id| id.as_str() == "test1"));
}

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

    // Create a test list file
    let test_list_path = temp.path().join("tests.txt");
    let mut file = fs::File::create(&test_list_path).unwrap();
    writeln!(file, "test1").unwrap();
    writeln!(file, "test3").unwrap();
    writeln!(file, "test5").unwrap();

    // Parse and verify
    let test_ids = inquest::testlist::parse_list_file(&test_list_path).unwrap();
    assert_eq!(test_ids.len(), 3);
    assert_eq!(test_ids[0].as_str(), "test1");
    assert_eq!(test_ids[1].as_str(), "test3");
    assert_eq!(test_ids[2].as_str(), "test5");
}

#[test]
fn test_workflow_times_database() {
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path));
    init_cmd.execute(&mut ui).unwrap();

    let factory = inquest::repository::inquest::InquestRepositoryFactory;
    let mut repo = factory.open(temp.path()).unwrap();

    // Insert run with durations
    let mut run = TestRun::new("0".to_string());
    run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
    run.add_result(
        TestResult::success("test1").with_duration(std::time::Duration::from_secs_f64(1.5)),
    );
    run.add_result(
        TestResult::success("test2").with_duration(std::time::Duration::from_secs_f64(0.3)),
    );

    repo.insert_test_run(run).unwrap();

    // Verify times were stored in the database
    let test_ids = vec![
        inquest::repository::TestId::new("test1"),
        inquest::repository::TestId::new("test2"),
    ];
    let times = repo.get_test_times_for_ids(&test_ids).unwrap();
    assert_eq!(times.len(), 2);
    assert_eq!(
        times
            .get(&inquest::repository::TestId::new("test1"))
            .unwrap()
            .as_secs_f64(),
        1.5
    );
    assert_eq!(
        times
            .get(&inquest::repository::TestId::new("test2"))
            .unwrap()
            .as_secs_f64(),
        0.3
    );
}

#[test]
fn test_workflow_list_flag() {
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize and populate repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    init_cmd.execute(&mut ui).unwrap();

    let factory = inquest::repository::inquest::InquestRepositoryFactory;
    let mut repo = factory.open(temp.path()).unwrap();

    // Add a run with failures
    let mut run = TestRun::new("0".to_string());
    run.timestamp = chrono::DateTime::from_timestamp(1000000000, 0).unwrap();
    run.add_result(TestResult::failure("test1", "Error"));
    run.add_result(TestResult::failure("test2", "Error"));
    run.add_result(TestResult::success("test3"));

    repo.insert_test_run_partial(run, false).unwrap();

    // Test --list flag
    let mut ui = TestUI::new();
    let failing_cmd = FailingCommand::with_list_only(Some(base_path));
    let result = failing_cmd.execute(&mut ui);
    assert_eq!(result.unwrap(), 1); // Exit code 1 when there are failures

    // Should output test IDs only, one per line
    assert_eq!(ui.output.len(), 2);
    // Order might vary, so check both are present
    assert!(ui.output[0] == "test1" || ui.output[0] == "test2");
    assert!(ui.output[1] == "test1" || ui.output[1] == "test2");
    assert_ne!(ui.output[0], ui.output[1]);
}

#[test]
fn test_error_handling_no_repository() {
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Try to run last command without initializing
    let mut ui = TestUI::new();
    let last_cmd = LastCommand::new(Some(base_path));
    let result = last_cmd.execute(&mut ui);

    // Should fail with an error
    assert!(result.is_err());
}

#[test]
fn test_parallel_execution() {
    use inquest::commands::RunCommand;
    use inquest::repository::inquest::InquestRepositoryFactory;

    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let factory = InquestRepositoryFactory;
    factory.initialise(temp.path()).unwrap();

    // Create a simple test configuration that outputs subunit
    let config = r#"
[DEFAULT]
test_command=python3 -c "import sys; import time; sys.stdout.buffer.write(b'\xb3\x29\x00\x16test1\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb3'); sys.stdout.buffer.flush()"
"#;
    fs::write(temp.path().join(".testr.conf"), config).unwrap();

    // Run with parallel execution
    let mut ui = TestUI::new();
    let cmd = RunCommand::with_all_options(
        Some(base_path.clone()),
        false,   // partial
        false,   // failing
        false,   // force_init
        false,   // auto
        None,    // load_list
        Some(2), // concurrency
        false,   // until_failure
        false,   // isolated
        false,   // subunit
        false,   // all_output
        None,    // test_filters
        None,    // test_args
    );

    // Note: This test will fail to actually run because the command is synthetic
    // But it tests that the parallel code path is exercised
    let _result = cmd.execute(&mut ui);

    // The command should have at least attempted to run
    assert!(!ui.output.is_empty());
}

#[test]
fn test_parallel_execution_with_worker_tags() {
    use inquest::partition::partition_tests;
    use inquest::repository::TestId;
    use std::collections::HashMap;

    // Create a set of test IDs
    let test_ids = vec![
        TestId::new("test1"),
        TestId::new("test2"),
        TestId::new("test3"),
        TestId::new("test4"),
    ];

    // Partition across 2 workers
    let partitions = partition_tests(&test_ids, &HashMap::new(), 2);

    // Should create 2 partitions
    assert_eq!(partitions.len(), 2);

    // All tests should be accounted for
    let total_tests: usize = partitions.iter().map(|p| p.len()).sum();
    assert_eq!(total_tests, 4);

    // Each partition should have at least one test
    assert!(!partitions[0].is_empty());
    assert!(!partitions[1].is_empty());
}

#[test]
fn test_until_failure_flag_behavior() {
    use inquest::commands::RunCommand;
    use inquest::repository::inquest::InquestRepositoryFactory;

    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let factory = InquestRepositoryFactory;
    factory.initialise(temp.path()).unwrap();

    // Create a simple test configuration that always succeeds
    let config = r#"
[DEFAULT]
test_command=echo ""
"#;
    fs::write(temp.path().join(".testr.conf"), config).unwrap();

    // Create command with until_failure set to true
    // The test will succeed but we can verify the flag was accepted
    // by checking that the command can be created
    let cmd = RunCommand::with_all_options(
        Some(base_path.clone()),
        false, // partial
        false, // failing
        false, // force_init
        false, // auto
        None,  // load_list
        None,  // concurrency
        true,  // until_failure
        false, // isolated
        false, // subunit
        false, // all_output
        None,  // test_filters
        None,  // test_args
    );

    // Verify the command was created successfully
    // (The actual looping behavior would run infinitely with always-passing tests,
    // so we just verify the command can be constructed with the flag)
    assert_eq!(cmd.name(), "run");
}

#[test]
fn test_isolated_flag_behavior() {
    use inquest::commands::RunCommand;
    use inquest::repository::inquest::InquestRepositoryFactory;

    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let factory = InquestRepositoryFactory;
    factory.initialise(temp.path()).unwrap();

    // Create a simple test configuration
    let config = r#"
[DEFAULT]
test_command=echo ""
"#;
    fs::write(temp.path().join(".testr.conf"), config).unwrap();

    // Create command with isolated set to true
    let cmd = RunCommand::with_all_options(
        Some(base_path.clone()),
        false, // partial
        false, // failing
        false, // force_init
        false, // auto
        None,  // load_list
        None,  // concurrency
        false, // until_failure
        true,  // isolated
        false, // subunit
        false, // all_output
        None,  // test_filters
        None,  // test_args
    );

    // Verify the command was created successfully
    assert_eq!(cmd.name(), "run");
}

#[test]
fn test_analyze_isolation_command_no_repository() {
    // Test that analyze-isolation gives proper error when repository doesn't exist
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    let mut ui = TestUI::new();
    let cmd = AnalyzeIsolationCommand::new(Some(base_path.clone()), "test_example".to_string());

    // Should fail because no repository exists
    let result = cmd.execute(&mut ui);
    assert!(result.is_err());
    assert!(result
        .unwrap_err()
        .to_string()
        .contains("Repository not found"));
}

#[test]
fn test_analyze_isolation_command_basic() {
    // Test that analyze-isolation command can be created and has correct metadata
    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    init_cmd.execute(&mut ui).unwrap();

    // Create .testr.conf with a simple command that outputs passing test
    let config = r#"
[DEFAULT]
test_command=printf "test: test_target\nsuccess: test_target\n" | python3 -c "import sys; sys.stdout.buffer.write(b'\xb3)\x00\x00\x01\x1btest: test_target\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\xb3*\x00\x00\x00\x1asuccess: test_target\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05')"
test_list_option=--list
"#;
    fs::write(temp.path().join(".testr.conf"), config).unwrap();

    // Create the analyze-isolation command
    let cmd = AnalyzeIsolationCommand::new(Some(base_path), "test_target".to_string());

    // Verify command metadata
    assert_eq!(cmd.name(), "analyze-isolation");
    assert_eq!(cmd.help(), "Analyze test isolation issues using bisection");
}

#[test]
fn test_group_regex_with_parallel_execution() {
    use inquest::testcommand::TestCommand;

    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    init_cmd.execute(&mut ui).unwrap();

    // Create .testr.conf with group_regex to group by module
    let config = r#"
[DEFAULT]
test_command=echo ""
test_list_option=--list
group_regex=^([^.]+)\.
"#;
    fs::write(temp.path().join(".testr.conf"), config).unwrap();

    // Load TestCommand and verify group_regex is set
    let test_cmd = TestCommand::from_directory(temp.path()).unwrap();
    assert_eq!(
        test_cmd.config().group_regex,
        Some("^([^.]+)\\.".to_string())
    );
}

#[test]
fn test_run_concurrency_callout() {
    use inquest::testcommand::TestCommand;

    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    init_cmd.execute(&mut ui).unwrap();

    // Create .testr.conf with test_run_concurrency
    let config = r#"
[DEFAULT]
test_command=echo ""
test_list_option=--list
test_run_concurrency=echo 2
"#;
    fs::write(temp.path().join(".testr.conf"), config).unwrap();

    // Load TestCommand and verify concurrency is determined from callout
    let test_cmd = TestCommand::from_directory(temp.path()).unwrap();
    let concurrency = test_cmd.get_concurrency().unwrap();
    assert_eq!(concurrency, Some(2));
}

#[test]
fn test_run_concurrency_callout_inquest_toml() {
    use inquest::testcommand::TestCommand;

    let temp = TempDir::new().unwrap();
    let base_path = temp.path().to_string_lossy().to_string();

    // Initialize repository
    let mut ui = TestUI::new();
    let init_cmd = InitCommand::new(Some(base_path.clone()));
    init_cmd.execute(&mut ui).unwrap();

    // Create inquest.toml instead of .testr.conf
    let config = r#"
test_command = "echo \"\""
test_list_option = "--list"
test_run_concurrency = "echo 2"
"#;
    fs::write(temp.path().join("inquest.toml"), config).unwrap();

    // Load TestCommand and verify it works with TOML config
    let test_cmd = TestCommand::from_directory(temp.path()).unwrap();
    let concurrency = test_cmd.get_concurrency().unwrap();
    assert_eq!(concurrency, Some(2));
}
