//! Test Generation for Purified Makefiles
//!
//! Generates comprehensive test suites for purified Makefiles to ensure:
//! - Determinism: Same inputs always produce same outputs
//! - Idempotency: Safe to re-run multiple times
//! - POSIX Compliance: Generated shell commands pass validation
//!
//! EXTREME TDD: This module implements Phase 2 (GREEN) to make CLI tests pass.

use std::path::Path;

/// Test generation options for Makefiles
#[derive(Debug, Clone)]
pub struct MakefileTestGeneratorOptions {
    /// 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 MakefileTestGeneratorOptions {
    fn default() -> Self {
        Self {
            property_tests: false,
            property_test_count: 100,
        }
    }
}

/// Test generator for purified Makefiles
pub struct MakefileTestGenerator {
    options: MakefileTestGeneratorOptions,
}

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

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

    /// Generate test suite for a purified Makefile
    ///
    /// # Arguments
    /// * `makefile_path` - Path to the purified Makefile
    /// * `_purified_content` - Content of the purified Makefile (for future analysis)
    ///
    /// # Returns
    /// Generated test suite as a String
    pub fn generate_tests(&self, makefile_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_makefile_name(makefile_path));
        test_suite.push('\n');
        test_suite.push_str("# Generated by bashrs make 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(makefile_path));
        test_suite.push('\n');

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

        // Test 3: POSIX Compliance
        test_suite.push_str(&self.generate_posix_compliance_test(makefile_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(makefile_path));
            test_suite.push('\n');
        }

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

        test_suite
    }

    /// Generate determinism test
    ///
    /// Tests that running make twice produces same outputs
    fn generate_determinism_test(&self, makefile_path: &Path) -> String {
        let makefile_name = Self::get_makefile_name(makefile_path);

        format!(
            r#"# Test: Determinism - same make invocation produces same output
test_determinism() {{
    printf "Testing determinism for {}...\n"

    # Run make twice and capture output
    make -f "{}" > /tmp/output1.txt 2>&1 || true
    make -f "{}" > /tmp/output2.txt 2>&1 || true

    # Sort outputs before comparing (handles make's parallel execution)
    sort /tmp/output1.txt > /tmp/output1_sorted.txt
    sort /tmp/output2.txt > /tmp/output2_sorted.txt

    # Compare sorted outputs
    if diff /tmp/output1_sorted.txt /tmp/output2_sorted.txt > /dev/null; then
        printf "✓ Determinism test passed\n"
        rm -f /tmp/output1.txt /tmp/output2.txt /tmp/output1_sorted.txt /tmp/output2_sorted.txt
        return 0
    else
        printf "✗ Determinism test failed - outputs differ\n"
        printf "First run (sorted):\n"
        cat /tmp/output1_sorted.txt
        printf "\nSecond run (sorted):\n"
        cat /tmp/output2_sorted.txt
        rm -f /tmp/output1.txt /tmp/output2.txt /tmp/output1_sorted.txt /tmp/output2_sorted.txt
        return 1
    fi
}}
"#,
            makefile_name, makefile_name, makefile_name
        )
    }

    /// Generate idempotency test
    ///
    /// Tests that running make multiple times is safe
    fn generate_idempotency_test(&self, makefile_path: &Path) -> String {
        let makefile_name = Self::get_makefile_name(makefile_path);

        format!(
            r#"# Test: Idempotency - safe to re-run multiple times
test_idempotency() {{
    printf "Testing idempotency for {}...\n"

    # Run make three times
    make -f "{}" > /dev/null 2>&1 || true
    make -f "{}" > /dev/null 2>&1 || exit_code1=$?
    make -f "{}" > /dev/null 2>&1 || exit_code2=$?

    # Second and third runs should succeed (exit code 0)
    if [ "${{exit_code1:-0}}" -eq 0 ] && [ "${{exit_code2:-0}}" -eq 0 ]; then
        printf "✓ Idempotency test passed\n"
        return 0
    else
        printf "✗ Idempotency test failed - not safe to re-run\n"
        return 1
    fi
}}
"#,
            makefile_name, makefile_name, makefile_name, makefile_name
        )
    }

    /// Generate POSIX compliance test
    ///
    /// Tests that Makefile follows POSIX conventions
    fn generate_posix_compliance_test(&self, makefile_path: &Path) -> String {
        let makefile_name = Self::get_makefile_name(makefile_path);

        format!(
            r#"# Test: POSIX Compliance - Makefile is POSIX-compatible
test_posix_compliance() {{
    printf "Testing POSIX compliance for {}...\n"

    # Check if Makefile can be parsed by POSIX make
    # (Most systems have GNU make, so we just verify it doesn't error)
    if make -f "{}" --version > /dev/null 2>&1; then
        printf "✓ POSIX compliance test passed\n"
        return 0
    else
        printf "⚠ Could not verify POSIX compliance (make may not be available)\n"
        return 0  # Don't fail if make is not available
    fi
}}
"#,
            makefile_name, makefile_name
        )
    }

    /// Generate property-based determinism tests
    ///
    /// Tests determinism across multiple randomized scenarios
    fn generate_property_determinism_test(&self, makefile_path: &Path) -> String {
        let makefile_name = Self::get_makefile_name(makefile_path);
        let count = self.options.property_test_count;

        format!(
            r#"# Test: Property-Based Determinism - {count} test cases
test_property_determinism() {{
    printf "Testing property-based determinism ({count} cases) for {}...\n"

    failed=0
    passed=0

    # Run make multiple times and verify determinism
    i=1
    while [ "$i" -le {count} ]; do
        make -f "{}" > "/tmp/prop_output_${{i}}.txt" 2>&1 || true
        sort "/tmp/prop_output_${{i}}.txt" > "/tmp/prop_output_${{i}}_sorted.txt"

        if [ "$i" -gt 1 ]; then
            if diff "/tmp/prop_output_1_sorted.txt" "/tmp/prop_output_${{i}}_sorted.txt" > /dev/null 2>&1; then
                passed=$((passed + 1))
            else
                failed=$((failed + 1))
            fi
        fi

        i=$((i + 1))
    done

    # Cleanup
    rm -f /tmp/prop_output_*.txt /tmp/prop_output_*_sorted.txt

    if [ "$failed" -eq 0 ]; then
        printf "✓ Property-based determinism test passed ($passed/{count} cases)\n"
        return 0
    else
        printf "✗ Property-based determinism test failed ($failed/{count} cases)\n"
        return 1
    fi
}}
"#,
            makefile_name, makefile_name
        )
    }

    /// Generate test runner
    ///
    /// Runs all tests and reports results
    fn generate_test_runner(&self) -> String {
        let mut script = String::from("# Test Runner\nrun_all_tests() {\n");
        script.push_str("    printf \"\\n=== Running Test Suite ===\\n\\n\"\n\n");
        script.push_str("    failed=0\n    passed=0\n\n");

        // Add core tests
        script.push_str(&self.generate_test_invocation("determinism"));
        script.push_str(&self.generate_test_invocation("idempotency"));
        script.push_str(&self.generate_test_invocation("posix_compliance"));

        // Add property tests if enabled
        if self.options.property_tests {
            script.push_str(&self.generate_test_invocation("property_determinism"));
        }

        // Add summary
        script.push_str(&self.generate_test_summary());
        script.push_str("}\n\n# Run tests\nrun_all_tests\n");

        script
    }

    fn generate_test_invocation(&self, test_name: &str) -> String {
        format!(
            r#"    # Run {0} test
    if test_{0}; then
        passed=$((passed + 1))
    else
        failed=$((failed + 1))
    fi

    printf "\n"

"#,
            test_name
        )
    }

    fn generate_test_summary(&self) -> String {
        r#"    printf "\n=== Test Summary ===\n"
    printf "Passed: %d\n" "$passed"
    printf "Failed: %d\n" "$failed"

    if [ "$failed" -eq 0 ]; then
        printf "\n✓ All tests passed!\n"
        return 0
    else
        printf "\n✗ Some tests failed\n"
        return 1
    fi
"#
        .to_string()
    }
}

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

    #[test]
    fn test_makefile_test_generator_basic() {
        let options = MakefileTestGeneratorOptions::default();
        let generator = MakefileTestGenerator::new(options);

        let makefile_path = PathBuf::from("test_makefile/Makefile");
        let purified_content = ".PHONY: all\nall:\n\techo test";

        let test_suite = generator.generate_tests(&makefile_path, purified_content);

        // Verify shebang
        assert!(test_suite.starts_with("#!/bin/sh"));

        // Verify test names
        assert!(test_suite.contains("test_determinism"));
        assert!(test_suite.contains("test_idempotency"));
        assert!(test_suite.contains("test_posix_compliance"));

        // Verify test runner
        assert!(test_suite.contains("run_all_tests"));

        // Should not contain property tests (not enabled)
        assert!(!test_suite.contains("test_property_determinism"));
    }

    #[test]
    fn test_makefile_test_generator_with_property_tests() {
        let options = MakefileTestGeneratorOptions {
            property_tests: true,
            property_test_count: 50,
        };
        let generator = MakefileTestGenerator::new(options);

        let makefile_path = PathBuf::from("Makefile");
        let purified_content = ".PHONY: build\nbuild:\n\techo build";

        let test_suite = generator.generate_tests(&makefile_path, purified_content);

        // Verify property tests included
        assert!(test_suite.contains("test_property_determinism"));
        assert!(test_suite.contains("50 test cases"));
        assert!(test_suite.contains("50 cases"));
    }

    #[test]
    fn test_get_makefile_name() {
        let path = PathBuf::from("/tmp/test/Makefile");
        assert_eq!(MakefileTestGenerator::get_makefile_name(&path), "Makefile");

        let path2 = PathBuf::from("MyMakefile");
        assert_eq!(
            MakefileTestGenerator::get_makefile_name(&path2),
            "MyMakefile"
        );
    }

    #[test]
    fn test_determinism_test_generation() {
        let options = MakefileTestGeneratorOptions::default();
        let generator = MakefileTestGenerator::new(options);

        let makefile_path = PathBuf::from("Makefile");
        let test = generator.generate_determinism_test(&makefile_path);

        // Verify test structure
        assert!(test.contains("test_determinism()"));
        assert!(test.contains("make -f"));
        assert!(test.contains("/tmp/output1.txt"));
        assert!(test.contains("/tmp/output2.txt"));
        assert!(test.contains("diff"));
    }

    #[test]
    fn test_idempotency_test_generation() {
        let options = MakefileTestGeneratorOptions::default();
        let generator = MakefileTestGenerator::new(options);

        let makefile_path = PathBuf::from("test.mk");
        let test = generator.generate_idempotency_test(&makefile_path);

        // Verify test structure
        assert!(test.contains("test_idempotency()"));
        assert!(test.contains("make -f \"test.mk\""));
        assert!(test.matches("make -f").count() >= 3); // Should run make 3 times
    }

    #[test]
    fn test_posix_compliance_test_generation() {
        let options = MakefileTestGeneratorOptions::default();
        let generator = MakefileTestGenerator::new(options);

        let makefile_path = PathBuf::from("Makefile");
        let test = generator.generate_posix_compliance_test(&makefile_path);

        // Verify test structure
        assert!(test.contains("test_posix_compliance()"));
        assert!(test.contains("POSIX"));
    }

    #[test]
    fn test_property_test_generation() {
        let options = MakefileTestGeneratorOptions {
            property_tests: true,
            property_test_count: 100,
        };
        let generator = MakefileTestGenerator::new(options);

        let makefile_path = PathBuf::from("Makefile");
        let test = generator.generate_property_determinism_test(&makefile_path);

        // Verify test structure
        assert!(test.contains("test_property_determinism()"));
        assert!(test.contains("100 test cases"));
        assert!(test.contains("while"));
        assert!(test.contains("100"));
    }

    #[test]
    fn test_runner_without_property_tests() {
        let options = MakefileTestGeneratorOptions::default();
        let generator = MakefileTestGenerator::new(options);

        let runner = generator.generate_test_runner();

        // Verify runner calls all tests
        assert!(runner.contains("test_determinism"));
        assert!(runner.contains("test_idempotency"));
        assert!(runner.contains("test_posix_compliance"));
        assert!(!runner.contains("test_property_determinism"));
        assert!(runner.contains("run_all_tests"));
    }

    #[test]
    fn test_runner_with_property_tests() {
        let options = MakefileTestGeneratorOptions {
            property_tests: true,
            property_test_count: 50,
        };
        let generator = MakefileTestGenerator::new(options);

        let runner = generator.generate_test_runner();

        // Verify runner calls all tests including property tests
        assert!(runner.contains("test_determinism"));
        assert!(runner.contains("test_idempotency"));
        assert!(runner.contains("test_posix_compliance"));
        assert!(runner.contains("test_property_determinism"));
    }

    // ============================================================================
    // Property-Based Tests (EXTREME TDD)
    // ============================================================================

    use proptest::prelude::*;

    proptest! {
        /// Property: Generated test suite always starts with POSIX shebang
        #[test]
        fn prop_test_suite_has_posix_shebang(
            property_tests in proptest::bool::ANY,
            property_test_count in 1usize..200,
        ) {
            let options = MakefileTestGeneratorOptions {
                property_tests,
                property_test_count,
            };
            let generator = MakefileTestGenerator::new(options);
            let makefile_path = PathBuf::from("Makefile");
            let purified = ".PHONY: all\nall:\n\techo test";

            let test_suite = generator.generate_tests(&makefile_path, purified);

            prop_assert!(test_suite.starts_with("#!/bin/sh"));
        }

        /// Property: Test suite always contains all three core tests
        #[test]
        fn prop_test_suite_contains_core_tests(
            property_tests in proptest::bool::ANY,
            property_test_count in 1usize..200,
        ) {
            let options = MakefileTestGeneratorOptions {
                property_tests,
                property_test_count,
            };
            let generator = MakefileTestGenerator::new(options);
            let makefile_path = PathBuf::from("test.mk");
            let purified = ".PHONY: build\nbuild:\n\techo build";

            let test_suite = generator.generate_tests(&makefile_path, purified);

            prop_assert!(test_suite.contains("test_determinism"));
            prop_assert!(test_suite.contains("test_idempotency"));
            prop_assert!(test_suite.contains("test_posix_compliance"));
        }

        /// Property: Property tests included if and only if enabled
        #[test]
        fn prop_property_tests_conditional(
            property_tests in proptest::bool::ANY,
            property_test_count in 1usize..200,
        ) {
            let options = MakefileTestGeneratorOptions {
                property_tests,
                property_test_count,
            };
            let generator = MakefileTestGenerator::new(options);
            let makefile_path = PathBuf::from("Makefile");
            let purified = ".PHONY: clean\nclean:\n\trm -f *.o";

            let test_suite = generator.generate_tests(&makefile_path, purified);

            if property_tests {
                prop_assert!(test_suite.contains("test_property_determinism"));
            } else {
                prop_assert!(!test_suite.contains("test_property_determinism"));
            }
        }

        /// Property: Test suite never panics (generation is always safe)
        #[test]
        fn prop_test_generation_never_panics(
            property_tests in proptest::bool::ANY,
            property_test_count in 1usize..500,
            makefile_name in "[a-zA-Z0-9_-]{1,50}\\.(mk|makefile|Makefile)",
        ) {
            let options = MakefileTestGeneratorOptions {
                property_tests,
                property_test_count,
            };
            let generator = MakefileTestGenerator::new(options);
            let makefile_path = PathBuf::from(makefile_name);
            let purified = ".PHONY: test\ntest:\n\techo ok";

            // Should never panic
            let _ = generator.generate_tests(&makefile_path, purified);
        }

        /// Property: Generated test count matches configuration
        #[test]
        fn prop_property_test_count_correct(
            property_test_count in 10usize..200,
        ) {
            let options = MakefileTestGeneratorOptions {
                property_tests: true,
                property_test_count,
            };
            let generator = MakefileTestGenerator::new(options);
            let makefile_path = PathBuf::from("Makefile");
            let purified = ".PHONY: all\nall:\n\t@echo done";

            let test_suite = generator.generate_tests(&makefile_path, purified);

            // Should contain the count in the test output
            let expected_text = format!("{} test cases", property_test_count);
            prop_assert!(test_suite.contains(&expected_text), "Missing test count: {}", expected_text);
        }

        /// Property: Test suite is valid shell (contains function definitions)
        #[test]
        fn prop_test_suite_is_valid_shell(
            property_tests in proptest::bool::ANY,
            property_test_count in 1usize..200,
        ) {
            let options = MakefileTestGeneratorOptions {
                property_tests,
                property_test_count,
            };
            let generator = MakefileTestGenerator::new(options);
            let makefile_path = PathBuf::from("build.mk");
            let purified = "all:\n\techo build";

            let test_suite = generator.generate_tests(&makefile_path, purified);

            // Should contain shell function syntax
            let function_syntax = "() {";
            prop_assert!(test_suite.contains(function_syntax), "Missing function syntax");
            prop_assert!(test_suite.contains("run_all_tests"), "Missing run_all_tests function");
        }

        /// Property: Determinism test always runs make twice
        #[test]
        fn prop_determinism_test_runs_make_twice(
            property_tests in proptest::bool::ANY,
        ) {
            let options = MakefileTestGeneratorOptions {
                property_tests,
                property_test_count: 100,
            };
            let generator = MakefileTestGenerator::new(options);
            let makefile_path = PathBuf::from("Makefile");

            let determinism_test = generator.generate_determinism_test(&makefile_path);

            // Should run make at least twice for comparison
            prop_assert!(determinism_test.matches("make -f").count() >= 2);
        }
    }
}
