bashrs 6.66.0

Rust-to-Shell transpiler for deterministic bootstrap scripts
//! Test Generation for Purified Bash Scripts
//!
//! Generates comprehensive test suites for purified bash scripts to ensure:
//! - Determinism: Same inputs always produce same outputs
//! - Idempotency: Safe to re-run multiple times
//! - POSIX Compliance: Generated tests pass shellcheck
//!
//! EXTREME TDD: This module implements Phase 2 (GREEN) to make CLI tests pass.

use std::path::Path;

/// Test generation options
#[derive(Debug, Clone)]
pub struct TestGeneratorOptions {
    /// Generate property-based tests (100+ cases)
    pub property_tests: bool,

    /// Number of property test cases to generate
    pub property_test_count: usize,
}

impl Default for TestGeneratorOptions {
    fn default() -> Self {
        Self {
            property_tests: false,
            property_test_count: 100,
        }
    }
}

/// Test generator for purified bash scripts
pub struct TestGenerator {
    options: TestGeneratorOptions,
}

impl TestGenerator {
    /// Create a new test generator with given options
    pub fn new(options: TestGeneratorOptions) -> Self {
        Self { options }
    }

    /// Extract script name from path
    #[allow(clippy::expect_used)]
    fn get_script_name(script_path: &Path) -> &str {
        script_path
            .file_name()
            .expect("Script path should have a file name")
            .to_str()
            .expect("File name should be valid UTF-8")
    }

    /// Generate test suite for a purified script
    ///
    /// # Arguments
    /// * `script_path` - Path to the purified script file
    /// * `_purified_content` - Content of the purified script (for future analysis)
    ///
    /// # Returns
    /// Generated test suite as a String
    pub fn generate_tests(&self, script_path: &Path, _purified_content: &str) -> String {
        let mut test_suite = String::new();

        // Shebang
        test_suite.push_str("#!/bin/sh\n");
        test_suite.push_str("# Test Suite for ");
        test_suite.push_str(Self::get_script_name(script_path));
        test_suite.push('\n');
        test_suite.push_str("# Generated by bashrs purify --with-tests\n\n");
        test_suite.push_str("set -e  # Exit on first failure\n\n");

        // Test 1: Determinism
        test_suite.push_str(&self.generate_determinism_test(script_path));
        test_suite.push('\n');

        // Test 2: Idempotency
        test_suite.push_str(&self.generate_idempotency_test(script_path));
        test_suite.push('\n');

        // Test 3: POSIX Compliance
        test_suite.push_str(&self.generate_posix_compliance_test(script_path));
        test_suite.push('\n');

        // Test 4: Property-based tests (if enabled)
        if self.options.property_tests {
            test_suite.push_str(&self.generate_property_determinism_test(script_path));
            test_suite.push('\n');
        }

        // Test Runner
        test_suite.push_str(&self.generate_test_runner());

        test_suite
    }

    /// Generate determinism test
    ///
    /// Tests that running the script twice with same inputs produces same outputs
    fn generate_determinism_test(&self, script_path: &Path) -> String {
        let script_name = Self::get_script_name(script_path);

        format!(
            r#"# Test 1: Determinism - Same inputs produce same outputs
test_determinism() {{
    echo "Testing: Determinism..."

    # Run script twice with same inputs
    output1=$(./{} 2>&1 || true)
    output2=$(./{} 2>&1 || true)

    if [ "$output1" = "$output2" ]; then
        echo "  ✅ PASS: Outputs are identical"
        return 0
    else
        echo "  ❌ FAIL: Outputs differ"
        echo "    Run 1: $output1"
        echo "    Run 2: $output2"
        return 1
    fi
}}
"#,
            script_name, script_name
        )
    }

    /// Generate idempotency test
    ///
    /// Tests that the script can be run multiple times safely
    fn generate_idempotency_test(&self, script_path: &Path) -> String {
        let script_name = Self::get_script_name(script_path);

        format!(
            r#"# Test 2: Idempotency - Safe to re-run
test_idempotency() {{
    echo "Testing: Idempotency..."

    # First run
    ./{} >/dev/null 2>&1 || {{
        echo "  ⚠️  SKIP: Script requires specific setup"
        return 0
    }}

    # Second run (should not fail)
    ./{} >/dev/null 2>&1 || {{
        echo "  ❌ FAIL: Second run failed (not idempotent)"
        return 1
    }}

    echo "  ✅ PASS: Script is idempotent"
    return 0
}}
"#,
            script_name, script_name
        )
    }

    /// Generate POSIX compliance test
    ///
    /// Tests that the purified script passes shellcheck
    fn generate_posix_compliance_test(&self, script_path: &Path) -> String {
        let script_name = Self::get_script_name(script_path);

        format!(
            r#"# Test 3: POSIX Compliance
test_posix_compliance() {{
    echo "Testing: POSIX Compliance..."

    if command -v shellcheck >/dev/null 2>&1; then
        shellcheck -s sh ./{} || {{
            echo "  ❌ FAIL: shellcheck found POSIX violations"
            return 1
        }}
        echo "  ✅ PASS: POSIX compliant (shellcheck)"
    else
        echo "  ⚠️  SKIP: shellcheck not installed"
    fi
    return 0
}}
"#,
            script_name
        )
    }

    /// Generate property-based determinism test
    ///
    /// Tests determinism with multiple generated inputs
    fn generate_property_determinism_test(&self, script_path: &Path) -> String {
        let script_name = Self::get_script_name(script_path);
        let test_count = self.options.property_test_count;

        format!(
            r#"# Property Test 4: Determinism holds for all inputs ({} cases)
test_property_determinism() {{
    echo "Testing: Determinism Property ({} cases)..."

    passed=0
    failed=0

    for i in $(seq 1 {}); do
        # Generate test input
        test_input="test_case_$i"

        # Run script twice with same input
        output1=$(./{} "$test_input" 2>&1 || true)
        output2=$(./{} "$test_input" 2>&1 || true)

        if [ "$output1" = "$output2" ]; then
            passed=$((passed + 1))
        else
            failed=$((failed + 1))
            echo "    ❌ Case $i failed (input=$test_input)"
        fi
    done

    if [ $failed -eq 0 ]; then
        echo "  ✅ PASS: Determinism property ($passed/{} cases)"
        return 0
    else
        echo "  ❌ FAIL: $failed/{} cases failed"
        return 1
    fi
}}
"#,
            test_count, test_count, test_count, script_name, script_name, test_count, test_count
        )
    }

    /// Generate test runner
    ///
    /// Runs all tests and reports results
    fn generate_test_runner(&self) -> String {
        let mut runner = String::new();

        runner.push_str("# Test Runner\n");
        runner.push_str("echo \"================================================\"\n");
        runner.push_str("echo \"bashrs purify --with-tests\"\n");
        runner.push_str("echo \"Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n");
        runner.push_str("echo \"================================================\"\n");
        runner.push_str("echo \"\"\n\n");

        runner.push_str("# Run all tests\n");
        runner.push_str("failed_tests=\"\"\n\n");

        runner.push_str("test_determinism || failed_tests=\"$failed_tests test_determinism\"\n");
        runner.push_str("test_idempotency || failed_tests=\"$failed_tests test_idempotency\"\n");
        runner.push_str(
            "test_posix_compliance || failed_tests=\"$failed_tests test_posix_compliance\"\n",
        );

        if self.options.property_tests {
            runner.push_str("test_property_determinism || failed_tests=\"$failed_tests test_property_determinism\"\n");
        }

        runner.push_str("\necho \"\"\n");
        runner.push_str("echo \"================================================\"\n\n");

        runner.push_str("if [ -z \"$failed_tests\" ]; then\n");
        runner.push_str("    echo \"✅ All tests passed!\"\n");
        runner.push_str("    exit 0\n");
        runner.push_str("else\n");
        runner.push_str("    echo \"❌ Failed tests:$failed_tests\"\n");
        runner.push_str("    exit 1\n");
        runner.push_str("fi\n");

        runner
    }
}

// Tests in bash_transpiler test modules