"""Tests for the self-healing test module."""

from pathlib import Path
from tempfile import TemporaryDirectory

from autodebug.healing.detector import (
    DetectionResult,
    FlakyDetector,
    analyze_flakiness_risk,
)
from autodebug.healing.languages import (
    SUPPORTED_LANGUAGES,
    GoAdapter,
    JavaAdapter,
    PythonAdapter,
    RustAdapter,
    TypeScriptAdapter,
    get_adapter,
    get_adapter_for_file,
)
from autodebug.healing.patterns import (
    BUILTIN_PATTERNS,
    PATTERNS_BY_ID,
    FixStrategy,
    PatternCategory,
    PatternMatch,
)


class TestPatterns:
    """Test pattern definitions."""

    def test_builtin_patterns_not_empty(self):
        """Verify builtin patterns are loaded."""
        assert len(BUILTIN_PATTERNS) > 0

    def test_all_categories_have_patterns(self):
        """Verify each category has at least one pattern."""
        # Rebuild the index to ensure it's up to date
        patterns_by_cat = {}
        for pattern in BUILTIN_PATTERNS:
            if pattern.category not in patterns_by_cat:
                patterns_by_cat[pattern.category] = []
            patterns_by_cat[pattern.category].append(pattern)

        for category in PatternCategory:
            assert category in patterns_by_cat, f"No patterns for {category}"
            assert len(patterns_by_cat[category]) > 0, f"Empty patterns for {category}"

    def test_pattern_ids_unique(self):
        """Verify all pattern IDs are unique."""
        ids = [p.id for p in BUILTIN_PATTERNS]
        assert len(ids) == len(set(ids)), "Duplicate pattern IDs found"

    def test_pattern_has_required_fields(self):
        """Verify all patterns have required fields."""
        for pattern in BUILTIN_PATTERNS:
            assert isinstance(pattern.id, str) and len(pattern.id) > 0, f"Pattern missing id: {pattern}"
            assert isinstance(pattern.name, str) and len(pattern.name) > 0, f"Pattern {pattern.id} missing name"
            assert pattern.category in PatternCategory, f"Pattern {pattern.id} has invalid category: {pattern.category}"
            assert isinstance(pattern.description, str) and len(pattern.description) > 0, f"Pattern {pattern.id} missing description"
            assert 1 <= pattern.severity <= 5, f"Pattern {pattern.id} severity {pattern.severity} out of range [1, 5]"
            assert len(pattern.code_patterns) > 0, f"Pattern {pattern.id} has no code_patterns"

    def test_lookup_by_id(self):
        """Test pattern lookup by ID."""
        pattern = PATTERNS_BY_ID.get("async-wait-sleep")
        assert pattern is not None
        assert pattern.name == "Hardcoded Sleep"
        assert pattern.category == PatternCategory.ASYNC_WAIT

    def test_platform_patterns_exist(self):
        """Verify PLATFORM category has patterns."""
        # Check in BUILTIN_PATTERNS directly
        platform_patterns = [p for p in BUILTIN_PATTERNS if p.category == PatternCategory.PLATFORM]
        assert len(platform_patterns) > 0
        # Check for specific platform patterns
        ids = {p.id for p in platform_patterns}
        assert "platform-path-separator" in ids
        assert "platform-line-ending" in ids
        assert "platform-temp-path" in ids


class TestLanguageAdapters:
    """Test language adapters."""

    def test_all_supported_languages_have_adapters(self):
        """Verify all supported languages have adapters."""
        for lang in SUPPORTED_LANGUAGES:
            adapter = get_adapter(lang)
            assert adapter is not None, f"No adapter for {lang}"

    def test_python_adapter(self):
        """Test Python adapter."""
        adapter = get_adapter("python")
        assert adapter is not None
        assert isinstance(adapter, PythonAdapter)
        assert ".py" in adapter.extensions
        assert adapter.comment_prefix == "#"

    def test_typescript_adapter(self):
        """Test TypeScript adapter."""
        adapter = get_adapter("typescript")
        assert adapter is not None
        assert isinstance(adapter, TypeScriptAdapter)
        assert ".ts" in adapter.extensions
        assert adapter.comment_prefix == "//"

    def test_go_adapter(self):
        """Test Go adapter."""
        adapter = get_adapter("go")
        assert adapter is not None
        assert isinstance(adapter, GoAdapter)
        assert ".go" in adapter.extensions

    def test_java_adapter(self):
        """Test Java adapter."""
        adapter = get_adapter("java")
        assert adapter is not None
        assert isinstance(adapter, JavaAdapter)
        assert ".java" in adapter.extensions

    def test_rust_adapter(self):
        """Test Rust adapter."""
        adapter = get_adapter("rust")
        assert adapter is not None
        assert isinstance(adapter, RustAdapter)
        assert ".rs" in adapter.extensions
        assert adapter.comment_prefix == "//"

    def test_get_adapter_for_file(self):
        """Test getting adapter by file extension."""
        assert isinstance(get_adapter_for_file(Path("test.py")), PythonAdapter)
        assert isinstance(get_adapter_for_file(Path("test.ts")), TypeScriptAdapter)
        assert isinstance(get_adapter_for_file(Path("test.go")), GoAdapter)
        assert isinstance(get_adapter_for_file(Path("test.java")), JavaAdapter)
        assert isinstance(get_adapter_for_file(Path("test.rs")), RustAdapter)
        assert get_adapter_for_file(Path("test.unknown")) is None


class TestDetector:
    """Test flaky pattern detection."""

    def test_detect_sleep_pattern_python(self):
        """Test detecting time.sleep in Python code."""
        code = """
import time

def test_something():
    do_async_operation()
    time.sleep(5)  # Wait for operation to complete
    assert result == expected
"""
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_example.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.3)
            result = detector.detect_in_file(test_file)

            assert result.files_scanned == 1
            sleep_matches = [m for m in result.matches if m.pattern.id == "async-wait-sleep"]
            assert len(sleep_matches) >= 1

    def test_detect_global_state_pattern(self):
        """Test detecting global state mutation."""
        code = """
import os

def test_with_env():
    os.environ['MY_VAR'] = 'test_value'
    run_test()
    assert something == True
"""
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_env.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.3)
            result = detector.detect_in_file(test_file)

            global_matches = [m for m in result.matches if m.pattern.id == "order-dep-global-state"]
            assert len(global_matches) >= 1

    def test_detect_hardcoded_temp_path(self):
        """Test detecting hardcoded temp directory."""
        code = """
def test_write_file():
    with open('/tmp/test_file.txt', 'w') as f:
        f.write('test')
"""
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_temp.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.3)
            result = detector.detect_in_file(test_file)

            temp_matches = [m for m in result.matches if m.pattern.id == "platform-temp-path"]
            assert len(temp_matches) >= 1

    def test_no_matches_for_clean_code(self):
        """Test that clean code has no matches."""
        code = """
import pytest
from pathlib import Path

def test_with_tmp_path(tmp_path):
    # Using pytest's tmp_path fixture - no flakiness
    test_file = tmp_path / "test.txt"
    test_file.write_text("hello")
    assert test_file.read_text() == "hello"
"""
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_clean.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.5)
            result = detector.detect_in_file(test_file)

            # Should have very few or no high-confidence matches
            high_confidence = [m for m in result.matches if m.confidence >= 0.5]
            assert len(high_confidence) == 0

    def test_analyze_flakiness_risk_with_matches(self):
        """Test flakiness risk analysis with matches."""
        # Create a result with some matches
        pattern = PATTERNS_BY_ID["async-wait-sleep"]
        match = PatternMatch(
            pattern=pattern,
            file_path="/test/file.py",
            line_number=10,
            column=0,
            matched_code="time.sleep(5)",
            context_before=[],
            context_after=[],
            confidence=0.9,
            language="python",
        )
        result = DetectionResult(
            matches=[match],
            files_scanned=1,
            files_with_matches=1,
        )

        analysis = analyze_flakiness_risk(result)
        assert analysis["risk_score"] > 0
        assert len(analysis["recommendations"]) > 0
        assert "ASYNC_WAIT" in analysis["by_category"]

    def test_analyze_flakiness_risk_no_matches(self):
        """Test flakiness risk analysis with no matches."""
        result = DetectionResult()
        analysis = analyze_flakiness_risk(result)

        assert analysis["risk_level"] == "low"
        assert analysis["risk_score"] == 0
        assert "No flaky patterns detected" in analysis["summary"]


class TestPatternMatch:
    """Test PatternMatch data class."""

    def test_location_property(self):
        """Test location property formatting."""
        pattern = BUILTIN_PATTERNS[0]
        match = PatternMatch(
            pattern=pattern,
            file_path="/path/to/file.py",
            line_number=42,
            column=8,
            matched_code="time.sleep(5)",
            context_before=["line1", "line2"],
            context_after=["line4", "line5"],
            confidence=0.8,
            language="python",
        )
        assert match.location == "/path/to/file.py:42:8"


class TestDetectionResult:
    """Test DetectionResult data class."""

    def test_empty_result(self):
        """Test empty detection result."""
        result = DetectionResult()
        assert result.total_matches == 0
        assert result.files_scanned == 0
        assert len(result.high_severity_matches) == 0

    def test_merge_results(self):
        """Test merging detection results."""
        result1 = DetectionResult(files_scanned=5, files_with_matches=2)
        result2 = DetectionResult(files_scanned=3, files_with_matches=1)

        merged = result1.merge(result2)
        assert merged.files_scanned == 8
        assert merged.files_with_matches == 3

    def test_get_summary(self):
        """Test getting result summary."""
        result = DetectionResult(files_scanned=10, files_with_matches=3)
        summary = result.get_summary()

        assert "total_matches" in summary
        assert "files_scanned" in summary
        assert summary["files_scanned"] == 10


class TestFixTemplates:
    """Test fix templates."""

    def test_fix_templates_have_strategies(self):
        """Verify fix templates have valid strategies."""
        for pattern in BUILTIN_PATTERNS:
            for fix in pattern.fix_templates:
                assert fix.strategy in FixStrategy
                assert isinstance(fix.description, str) and len(fix.description) > 0, f"Fix template missing description in pattern {pattern.id}"
                assert 0 < fix.confidence <= 1, f"Fix template confidence {fix.confidence} out of range (0, 1] in pattern {pattern.id}"

    def test_fix_templates_have_language_coverage(self):
        """Verify fix templates cover main languages."""
        # At minimum, Python should be covered for most patterns
        for pattern in BUILTIN_PATTERNS:
            if pattern.fix_templates:
                for fix in pattern.fix_templates:
                    if fix.templates:
                        # At least one language should be covered
                        assert len(fix.templates) > 0


class TestFalsePositiveSuppression:
    """Verify that known false positive scenarios are NOT flagged."""

    def test_docstring_sleep_not_flagged(self):
        """time.sleep mentioned in docstrings should not trigger Hardcoded Sleep."""
        code = '''
class TestNoSleepInTests:
    """Tests should not use time.sleep() as it causes flakiness."""

    def test_no_sleep_calls(self):
        """No test file uses time.sleep()."""
        violations = []
        if "time.sleep" in content:
            violations.append(file)
        assert len(violations) == 0
'''
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_quality.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.3)
            result = detector.detect_in_file(test_file)

            sleep_matches = [m for m in result.matches if m.pattern.id == "async-wait-sleep"]
            assert len(sleep_matches) == 0, (
                f"Docstring/string reference to time.sleep should not be flagged, "
                f"got {len(sleep_matches)} match(es)"
            )

    def test_subprocess_timeout_not_flagged(self):
        """subprocess.run(..., timeout=5) should not trigger Fixed Timeout Value."""
        code = '''
import subprocess

def _safe_read(path):
    result = subprocess.run(
        ["git", "show", f"HEAD:{path}"],
        capture_output=True,
        text=True,
        timeout=5,
    )
    return result.stdout
'''
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_helper.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.5)
            result = detector.detect_in_file(test_file)

            timeout_matches = [m for m in result.matches if m.pattern.id == "async-wait-fixed-timeout"]
            assert len(timeout_matches) == 0, (
                f"subprocess.run timeout should not be flagged, "
                f"got {len(timeout_matches)} match(es)"
            )

    def test_time_arithmetic_not_flagged(self):
        """time.time() used for state manipulation should not trigger Wall Clock Dependency."""
        code = '''
import time

def test_half_open_after_timeout():
    cb = CircuitBreaker(failure_threshold=1, recovery_timeout=0.1)
    cb._last_failure_time = time.time() - 0.2
    assert cb.state == CircuitBreaker.HALF_OPEN
'''
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_circuit.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.5)
            result = detector.detect_in_file(test_file)

            wall_clock_matches = [m for m in result.matches if m.pattern.id == "time-dep-wall-clock"]
            assert len(wall_clock_matches) == 0, (
                f"time.time() - N state manipulation should not be flagged, "
                f"got {len(wall_clock_matches)} match(es)"
            )

    def test_with_as_variable_not_flagged(self):
        """Variables from with...as should not trigger Shared Mutable State."""
        code = '''
def test_spinner_json_mode():
    fmt = OutputFormatter(json_mode=True)
    with fmt.progress_spinner("Working...") as status:
        status.update("Still working...")
'''
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_output.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.3)
            result = detector.detect_in_file(test_file)

            # The .update() call on a with-as local should be suppressed
            shared_state_matches = [
                m for m in result.matches
                if m.pattern.id == "order-dep-shared-mutable-state"
                and "update" in m.matched_code
            ]
            assert len(shared_state_matches) == 0, (
                f"with...as variable .update() should not be flagged, "
                f"got {len(shared_state_matches)} match(es)"
            )

    def test_environ_try_finally_not_flagged(self):
        """os.environ inside try/finally with cleanup should not trigger Global State Mutation."""
        code = '''
import os

def test_env_guard():
    guard = EnvGuard(allowed={})
    guard.install()
    try:
        os.environ["MY_VAR"] = "test"
        result = run_thing()
    finally:
        guard.uninstall()
'''
        with TemporaryDirectory() as tmpdir:
            test_file = Path(tmpdir) / "test_env.py"
            test_file.write_text(code)

            detector = FlakyDetector(min_confidence=0.3)
            result = detector.detect_in_file(test_file)

            global_matches = [m for m in result.matches if m.pattern.id == "order-dep-global-state"]
            assert len(global_matches) == 0, (
                f"os.environ with try/finally cleanup should not be flagged, "
                f"got {len(global_matches)} match(es)"
            )

    def test_all_categories_have_recommendations(self):
        """Verify all pattern categories produce recommendations."""
        from autodebug.healing.detector import _CATEGORY_RECOMMENDATIONS
        for cat in PatternCategory:
            assert cat in _CATEGORY_RECOMMENDATIONS, (
                f"Category {cat.name} missing from _CATEGORY_RECOMMENDATIONS"
            )
