"""Tests for the pattern database module."""


import pytest

from autodebug.patterns import (
    CodePattern,
    ExceptionSignature,
    FixPattern,
    MatchConfig,
    PatternMatcher,
    PatternStore,
    StackFrame,
    anonymize_diff,
    anonymize_message,
    extract_exception_signature,
    extract_pattern_from_fix,
    format_match_report,
)


class TestStackFrame:
    """Test StackFrame data class."""

    def test_to_signature(self):
        """Test signature generation."""
        frame = StackFrame(
            file="/path/to/module.py",
            function="test_function",
            line=42,
            module="module",
        )
        sig = frame.to_signature()
        assert sig == "module.py:test_function"

    def test_to_signature_without_path(self):
        """Test signature with just filename."""
        frame = StackFrame(file="file.py", function="func", line=1)
        assert frame.to_signature() == "file.py:func"

    def test_to_dict_and_from_dict(self):
        """Test serialization round-trip."""
        frame = StackFrame(
            file="/path/to/module.py",
            function="test_function",
            line=42,
            module="module",
        )
        data = frame.to_dict()
        restored = StackFrame.from_dict(data)
        assert restored.file == frame.file
        assert restored.function == frame.function
        assert restored.line == frame.line
        assert restored.module == frame.module


class TestExceptionSignature:
    """Test ExceptionSignature data class."""

    def test_to_dict_and_from_dict(self):
        """Test serialization round-trip."""
        sig = ExceptionSignature(
            exception_type="KeyError",
            message_pattern="'{string}'",
            stack_signature="module.py:func|other.py:call",
            top_frames=[
                StackFrame(file="module.py", function="func", line=10),
                StackFrame(file="other.py", function="call", line=20),
            ],
        )
        data = sig.to_dict()
        restored = ExceptionSignature.from_dict(data)
        assert restored.exception_type == sig.exception_type
        assert restored.message_pattern == sig.message_pattern
        assert len(restored.top_frames) == 2


class TestCodePattern:
    """Test CodePattern data class."""

    def test_to_dict_and_from_dict(self):
        """Test serialization round-trip."""
        pattern = CodePattern(
            pattern_type="missing_null_check",
            language="python",
            code_signature="$var0[$var1]",
            ast_pattern={"type": "subscript"},
        )
        data = pattern.to_dict()
        restored = CodePattern.from_dict(data)
        assert restored.pattern_type == pattern.pattern_type
        assert restored.language == pattern.language
        assert restored.code_signature == pattern.code_signature


class TestFixPattern:
    """Test FixPattern data class."""

    def test_success_rate_calculation(self):
        """Test success rate calculation."""
        pattern = FixPattern(
            id="test",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="KeyError",
                message_pattern="'{string}'",
                stack_signature="test",
                top_frames=[],
            ),
            code_pattern=None,
            fix_type="add_null_check",
            fix_description="Add null check",
            fix_template=None,
            fix_diff_pattern="",
            success_count=8,
            failure_count=2,
        )
        assert pattern.success_rate == 0.8

    def test_success_rate_zero_uses(self):
        """Test success rate with no uses."""
        pattern = FixPattern(
            id="test",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="KeyError",
                message_pattern="'{string}'",
                stack_signature="test",
                top_frames=[],
            ),
            code_pattern=None,
            fix_type="add_null_check",
            fix_description="Add null check",
            fix_template=None,
            fix_diff_pattern="",
        )
        assert pattern.success_rate == 0.0

    def test_confidence_calculation(self):
        """Test Wilson score confidence calculation."""
        # Pattern with many successful uses should have high confidence
        pattern = FixPattern(
            id="test",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="KeyError",
                message_pattern="'{string}'",
                stack_signature="test",
                top_frames=[],
            ),
            code_pattern=None,
            fix_type="add_null_check",
            fix_description="Add null check",
            fix_template=None,
            fix_diff_pattern="",
            success_count=95,
            failure_count=5,
        )
        assert pattern.confidence > 0.8

    def test_to_dict_and_from_dict(self):
        """Test serialization round-trip."""
        pattern = FixPattern(
            id="test",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="KeyError",
                message_pattern="'{string}'",
                stack_signature="test",
                top_frames=[],
            ),
            code_pattern=CodePattern(
                pattern_type="null_check",
                language="python",
                code_signature="test",
            ),
            fix_type="add_null_check",
            fix_description="Add null check",
            fix_template="if {var} is not None:",
            fix_diff_pattern="+if $var0:",
            success_count=10,
            failure_count=2,
            tags=["python", "null-check"],
        )
        data = pattern.to_dict()
        restored = FixPattern.from_dict(data)
        assert restored.id == pattern.id
        assert restored.fix_type == pattern.fix_type
        assert restored.success_count == pattern.success_count


class TestPatternStore:
    """Test PatternStore SQLite backend."""

    @pytest.fixture
    def temp_store(self, tmp_path):
        """Create a temporary pattern store."""
        db_path = tmp_path / "test_patterns.db"
        return PatternStore(db_path)

    def test_add_and_get_pattern(self, temp_store):
        """Test adding and retrieving a pattern."""
        pattern = FixPattern(
            id="test-pattern-1",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="KeyError",
                message_pattern="'{string}'",
                stack_signature="test:func",
                top_frames=[],
            ),
            code_pattern=None,
            fix_type="add_null_check",
            fix_description="Add null check before key access",
            fix_template=None,
            fix_diff_pattern="+if key in dict:",
        )

        temp_store.add_pattern(pattern)
        retrieved = temp_store.get_pattern("test-pattern-1")

        assert retrieved is not None
        assert retrieved.id == pattern.id
        assert retrieved.fix_type == pattern.fix_type

    def test_find_by_exception_type(self, temp_store):
        """Test finding patterns by exception type."""
        for i in range(3):
            pattern = FixPattern(
                id=f"keyerror-{i}",
                created_at="2024-01-01T00:00:00Z",
                updated_at="2024-01-01T00:00:00Z",
                exception_signature=ExceptionSignature(
                    exception_type="KeyError",
                    message_pattern="'{string}'",
                    stack_signature=f"test{i}:func",
                    top_frames=[],
                ),
                code_pattern=None,
                fix_type="add_null_check",
                fix_description=f"Fix {i}",
                fix_template=None,
                fix_diff_pattern="",
                success_count=i * 10,
            )
            temp_store.add_pattern(pattern)

        results = temp_store.find_by_exception_type("KeyError")
        assert len(results) == 3
        # Should be ordered by success_count desc
        assert results[0].success_count >= results[1].success_count

    def test_record_usage(self, temp_store):
        """Test recording pattern usage."""
        pattern = FixPattern(
            id="usage-test",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="ValueError",
                message_pattern="",
                stack_signature="test",
                top_frames=[],
            ),
            code_pattern=None,
            fix_type="test",
            fix_description="Test",
            fix_template=None,
            fix_diff_pattern="",
            success_count=0,
            failure_count=0,
        )
        temp_store.add_pattern(pattern)

        temp_store.record_usage("usage-test", success=True)
        temp_store.record_usage("usage-test", success=True)
        temp_store.record_usage("usage-test", success=False)

        retrieved = temp_store.get_pattern("usage-test")
        assert retrieved.success_count == 2
        assert retrieved.failure_count == 1
        assert retrieved.total_uses == 3

    def test_get_stats(self, temp_store):
        """Test getting store statistics."""
        for i in range(5):
            pattern = FixPattern(
                id=f"stat-{i}",
                created_at="2024-01-01T00:00:00Z",
                updated_at="2024-01-01T00:00:00Z",
                exception_signature=ExceptionSignature(
                    exception_type="TestError",
                    message_pattern="",
                    stack_signature="test",
                    top_frames=[],
                ),
                code_pattern=None,
                fix_type="test_fix",
                fix_description="Test",
                fix_template=None,
                fix_diff_pattern="",
                success_count=10,
                failure_count=2,
                total_uses=12,
            )
            temp_store.add_pattern(pattern)

        stats = temp_store.get_stats()
        assert stats["total_patterns"] == 5
        assert stats["total_uses"] == 60  # 5 * 12

    def test_delete_pattern(self, temp_store):
        """Test deleting a pattern."""
        pattern = FixPattern(
            id="delete-test",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="Error",
                message_pattern="",
                stack_signature="test",
                top_frames=[],
            ),
            code_pattern=None,
            fix_type="test",
            fix_description="Test",
            fix_template=None,
            fix_diff_pattern="",
        )
        temp_store.add_pattern(pattern)
        assert temp_store.get_pattern("delete-test") is not None

        temp_store.delete_pattern("delete-test")
        assert temp_store.get_pattern("delete-test") is None

    def test_export_import(self, temp_store, tmp_path):
        """Test exporting and importing patterns."""
        # Add some patterns
        for i in range(3):
            pattern = FixPattern(
                id=f"export-{i}",
                created_at="2024-01-01T00:00:00Z",
                updated_at="2024-01-01T00:00:00Z",
                exception_signature=ExceptionSignature(
                    exception_type="TestError",
                    message_pattern="",
                    stack_signature="test",
                    top_frames=[],
                ),
                code_pattern=None,
                fix_type="test",
                fix_description=f"Test {i}",
                fix_template=None,
                fix_diff_pattern="",
            )
            temp_store.add_pattern(pattern)

        # Export
        export_path = tmp_path / "export.json"
        count = temp_store.export_patterns(export_path)
        assert count == 3
        assert export_path.exists()

        # Create new store and import
        new_store = PatternStore(tmp_path / "new_patterns.db")
        imported, skipped = new_store.import_patterns(export_path)
        assert imported == 3
        assert skipped == 0


class TestAnonymization:
    """Test anonymization functions."""

    def test_anonymize_message_with_string(self):
        """Test anonymizing string values in messages."""
        msg = "KeyError: 'user_id'"
        anon = anonymize_message(msg)
        assert "user_id" not in anon
        # Uses {string} placeholder
        assert "{string}" in anon

    def test_anonymize_message_with_number(self):
        """Test anonymizing numeric values."""
        msg = "IndexError: list index 42 out of range"
        anon = anonymize_message(msg)
        assert "42" not in anon
        # Uses {num} placeholder
        assert "{num}" in anon

    def test_anonymize_message_with_path(self):
        """Test anonymizing file paths."""
        msg = "FileNotFoundError: /home/user/test.py not found"
        anon = anonymize_message(msg)
        assert "/home/user" not in anon
        # Uses {path} placeholder
        assert "{path}" in anon

    def test_anonymize_diff(self):
        """Test anonymizing diffs."""
        diff = """
-    user_id = 12345
+    if user_id is not None:
+        user_id = 12345
"""
        anon = anonymize_diff(diff)
        # Numbers should be anonymized with {num}
        assert "12345" not in anon
        assert "{num}" in anon


class TestPatternMatcher:
    """Test pattern matching."""

    @pytest.fixture
    def store_with_patterns(self, tmp_path):
        """Create a store with some patterns for matching."""
        store = PatternStore(tmp_path / "matcher_test.db")

        pattern = FixPattern(
            id="keyerror-fix",
            created_at="2024-01-01T00:00:00Z",
            updated_at="2024-01-01T00:00:00Z",
            exception_signature=ExceptionSignature(
                exception_type="KeyError",
                message_pattern="'{string}'",
                stack_signature="handler.py:process_request",
                top_frames=[
                    StackFrame(file="handler.py", function="process_request", line=50),
                ],
            ),
            code_pattern=None,
            fix_type="add_dict_get",
            fix_description="Use dict.get() instead of direct access",
            fix_template="value = data.get({key}, default)",
            fix_diff_pattern="+value = data.get({string})",
            success_count=45,
            failure_count=3,
        )
        store.add_pattern(pattern)
        return store

    def test_find_matches_exact_exception_type(self, store_with_patterns):
        """Test finding matches with exact exception type."""
        matcher = PatternMatcher(store=store_with_patterns)

        exception_data = {
            "exc_type": "KeyError",
            "message": "'config_value'",
            "stack": [
                {"file": "handler.py", "function": "process_request", "line": 52},
            ],
        }

        matches = matcher.find_matches(exception_data)
        assert len(matches) >= 1
        assert matches[0].pattern.fix_type == "add_dict_get"

    def test_match_config(self):
        """Test MatchConfig defaults."""
        config = MatchConfig()
        assert config.min_similarity == 0.5
        assert config.max_results == 10
        assert config.stack_algorithm == "combined"

    def test_format_match_report_empty(self):
        """Test formatting empty match list."""
        report = format_match_report([])
        assert "No matching patterns found" in report


class TestExtractPattern:
    """Test pattern extraction from fixes."""

    def test_extract_pattern_from_fix(self):
        """Test extracting a pattern from a proven fix."""
        exception_data = {
            "exc_type": "KeyError",
            "message": "'user_id'",
            "stack": [
                {"file": "api/handlers.py", "function": "get_user", "line": 42},
                {"file": "api/routes.py", "function": "handle_request", "line": 100},
            ],
        }

        fix_diff = """
-    user_id = request.data['user_id']
+    user_id = request.data.get('user_id')
+    if user_id is None:
+        raise ValueError("Missing user_id")
"""

        pattern = extract_pattern_from_fix(
            exception_data=exception_data,
            fix_diff=fix_diff,
            fix_description="Use dict.get() with validation",
        )

        assert pattern is not None
        assert pattern.exception_signature.exception_type == "KeyError"
        assert pattern.fix_description == "Use dict.get() with validation"
        # Pattern ID is a random UUID assigned at extraction time
        assert pattern.id is not None

    def test_extract_exception_signature(self):
        """Test exception signature extraction."""
        exception_data = {
            "exc_type": "ValueError",
            "message": "invalid literal for int(): 'abc'",
            "stack": [
                {"file": "parser.py", "function": "parse_int", "line": 10},
            ],
        }

        sig = extract_exception_signature(exception_data)
        assert sig.exception_type == "ValueError"
        assert len(sig.top_frames) == 1
        assert sig.top_frames[0].function == "parse_int"
