"""Unit tests for error formatting functionality."""

import re
from datetime import datetime
from pathlib import Path

from src.autodoc_mcp.core.error_formatter import (
    ErrorFormatter,
    ErrorSeverity,
    FormattedError,
    ResponseFormatter,
)
from src.autodoc_mcp.exceptions import (
    NetworkError,
    PackageNotFoundError,
    ProjectParsingError,
)


class TestFormattedError:
    """Test FormattedError dataclass."""

    def test_formatted_error_creation(self):
        """Test basic FormattedError creation."""
        error = FormattedError(
            message="Test error message",
            suggestion="Test suggestion",
            severity=ErrorSeverity.ERROR,
            error_code="test_error",
            context={"key": "value"},
            recoverable=True,
        )

        assert error.message == "Test error message"
        assert error.suggestion == "Test suggestion"
        assert error.severity == ErrorSeverity.ERROR
        assert error.error_code == "test_error"
        assert error.context == {"key": "value"}
        assert error.recoverable is True

    def test_formatted_error_defaults(self):
        """Test FormattedError with default values."""
        error = FormattedError(message="Test message")

        assert error.message == "Test message"
        assert error.suggestion is None
        assert error.severity == ErrorSeverity.ERROR
        assert error.error_code is None
        assert error.context is None
        assert error.recoverable is True


class TestErrorSeverity:
    """Test ErrorSeverity enum."""

    def test_severity_values(self):
        """Test all severity level values."""
        assert ErrorSeverity.INFO.value == "info"
        assert ErrorSeverity.WARNING.value == "warning"
        assert ErrorSeverity.ERROR.value == "error"
        assert ErrorSeverity.CRITICAL.value == "critical"


class TestErrorFormatter:
    """Test ErrorFormatter functionality."""

    def test_dependency_patterns_defined(self):
        """Test that dependency patterns are properly defined."""
        patterns = ErrorFormatter.DEPENDENCY_PATTERNS

        assert len(patterns) > 0
        # Check pattern structure
        for pattern, error_type in patterns:
            assert isinstance(pattern, str)
            assert isinstance(error_type, str)
            # Ensure patterns compile
            re.compile(pattern)


class TestDependencyParsingErrors:
    """Test dependency parsing error formatting."""

    def test_format_dependency_parsing_errors_empty_list(self):
        """Test formatting empty list of dependency errors."""
        result = ErrorFormatter.format_dependency_parsing_errors([])
        assert result == []

    def test_format_dependency_parsing_errors_single_failure(self):
        """Test formatting single dependency failure."""
        failed_deps = [
            {
                "dependency_string": "invalid-pkg-name!@#",
                "error": "Invalid package name format: 'invalid-pkg-name!@#'",
                "source": "dependencies",
            }
        ]

        result = ErrorFormatter.format_dependency_parsing_errors(failed_deps)

        assert len(result) == 1
        assert isinstance(result[0], FormattedError)
        assert result[0].error_code == "invalid_package_name"
        assert "invalid-pkg-name" in result[0].message

    def test_format_dependency_parsing_errors_multiple_failures(self):
        """Test formatting multiple dependency failures."""
        failed_deps = [
            {
                "dependency_string": "pkg[unclosed-bracket",
                "error": "Unclosed bracket in extras: 'pkg[unclosed-bracket'",
                "source": "dev-dependencies",
            },
            {
                "dependency_string": "",
                "error": "Empty dependency string",
                "source": "dependencies",
            },
        ]

        result = ErrorFormatter.format_dependency_parsing_errors(failed_deps)

        assert len(result) == 2
        assert result[0].error_code == "malformed_extras"
        assert result[1].error_code == "empty_dependency"

    def test_format_single_dependency_error_invalid_package_name(self):
        """Test formatting invalid package name error."""
        result = ErrorFormatter._format_single_dependency_error(
            "bad@pkg", "Invalid package name format: 'bad@pkg'", "dependencies"
        )

        assert result.error_code == "invalid_package_name"
        assert "bad@pkg" in result.message
        assert "dependencies" in result.message
        assert result.severity == ErrorSeverity.WARNING
        assert result.recoverable is True
        assert "bad-pkg" in result.suggestion  # Should suggest fix

    def test_format_single_dependency_error_malformed_extras(self):
        """Test formatting malformed extras error."""
        result = ErrorFormatter._format_single_dependency_error(
            "pkg[extra", "Unclosed bracket in extras: 'pkg[extra'", "dev-dependencies"
        )

        assert result.error_code == "malformed_extras"
        assert "pkg[extra" in result.message
        assert "dev-dependencies" in result.message
        assert result.severity == ErrorSeverity.WARNING
        assert "square brackets" in result.suggestion

    def test_format_single_dependency_error_invalid_version(self):
        """Test formatting invalid version error."""
        result = ErrorFormatter._format_single_dependency_error(
            "pkg>>1.0", "Invalid version specifier.*'pkg>>1.0'", "dependencies"
        )

        assert result.error_code == "invalid_version"
        assert "pkg>>1.0" in result.message
        assert result.severity == ErrorSeverity.WARNING
        assert "valid version operators" in result.suggestion

    def test_format_single_dependency_error_empty_dependency(self):
        """Test formatting empty dependency error."""
        result = ErrorFormatter._format_single_dependency_error(
            "", "Empty dependency string", "dependencies"
        )

        assert result.error_code == "empty_dependency"
        assert "Empty dependency" in result.message
        assert "dependencies" in result.message
        assert result.severity == ErrorSeverity.WARNING

    def test_format_single_dependency_error_unknown_pattern(self):
        """Test formatting unknown dependency error pattern."""
        result = ErrorFormatter._format_single_dependency_error(
            "unknown-issue", "Some unknown error message", "dependencies"
        )

        assert result.error_code == "dependency_parse_unknown"
        assert "unknown-issue" in result.message
        assert "dependencies" in result.message
        assert result.severity == ErrorSeverity.WARNING
        assert result.recoverable is True

    def test_create_dependency_error_by_type_invalid_package_name(self):
        """Test creating invalid package name error."""
        match = re.search(
            r"Invalid package name format: '([^']+)'",
            "Invalid package name format: 'bad@name'",
        )

        result = ErrorFormatter._create_dependency_error_by_type(
            "invalid_package_name", "bad@name", "dependencies", match
        )

        assert result.error_code == "invalid_package_name"
        assert result.context["original_name"] == "bad@name"
        assert result.context["source"] == "dependencies"

    def test_create_dependency_error_by_type_malformed_extras(self, mocker):
        """Test creating malformed extras error."""
        match = mocker.Mock()
        match.groups.return_value = ()

        result = ErrorFormatter._create_dependency_error_by_type(
            "malformed_extras", "pkg[extra", "dev-dependencies", match
        )

        assert result.error_code == "malformed_extras"
        assert result.context["original_dep"] == "pkg[extra"
        assert result.context["source"] == "dev-dependencies"

    def test_create_dependency_error_by_type_fallback(self, mocker):
        """Test creating error with unknown type falls back gracefully."""
        match = mocker.Mock()

        result = ErrorFormatter._create_dependency_error_by_type(
            "unknown_type", "test-dep", "dependencies", match
        )

        assert result.error_code == "dependency_parse_unknown"
        assert "test-dep" in result.message


class TestSuggestionGeneration:
    """Test suggestion generation methods."""

    def test_suggest_package_name_fix_basic(self):
        """Test basic package name fix suggestions."""
        result = ErrorFormatter._suggest_package_name_fix("bad@name!")
        assert result == "bad-name"

    def test_suggest_package_name_fix_multiple_hyphens(self):
        """Test package name fix with multiple consecutive special chars."""
        result = ErrorFormatter._suggest_package_name_fix("bad---name@@@")
        assert result == "bad-name"

    def test_suggest_package_name_fix_leading_trailing_hyphens(self):
        """Test package name fix removes leading/trailing hyphens."""
        result = ErrorFormatter._suggest_package_name_fix("-bad-name-")
        assert result == "bad-name"

    def test_suggest_package_name_fix_empty_result(self):
        """Test package name fix fallback for empty result."""
        result = ErrorFormatter._suggest_package_name_fix("@@@")
        assert result == "package-name"

    def test_suggest_extras_fix_valid_package(self):
        """Test extras fix suggestion with valid package name."""
        result = ErrorFormatter._suggest_extras_fix("requests[unclosed")
        assert result == "requests[extra1,extra2]>=1.0.0"

    def test_suggest_extras_fix_no_package_match(self):
        """Test extras fix suggestion with no package match."""
        result = ErrorFormatter._suggest_extras_fix("[@#$")
        assert result == "package[extra1,extra2]>=1.0.0"

    def test_suggest_version_fix_valid_package(self):
        """Test version fix suggestion with valid package name."""
        result = ErrorFormatter._suggest_version_fix("requests>>invalid")
        assert result == "requests>=1.0.0"

    def test_suggest_version_fix_no_package_match(self):
        """Test version fix suggestion with no package match."""
        result = ErrorFormatter._suggest_version_fix("@@invalid")
        assert result == "package>=1.0.0"


class TestNetworkErrorFormatting:
    """Test network error formatting."""

    def test_format_network_errors_empty_list(self):
        """Test formatting empty list of network errors."""
        result = ErrorFormatter.format_network_errors([])
        assert result == []

    def test_format_network_errors_package_not_found(self):
        """Test formatting PackageNotFoundError."""
        errors = [
            {
                "type": "PackageNotFoundError",
                "package": "nonexistent-pkg",
                "message": "Package not found",
                "suggestions": ["requests", "httpx"],
            }
        ]

        result = ErrorFormatter.format_network_errors(errors)

        assert len(result) == 1
        assert result[0].error_code == "package_not_found"
        assert "nonexistent-pkg" in result[0].message
        assert "requests" in result[0].suggestion
        assert result[0].severity == ErrorSeverity.ERROR
        assert result[0].recoverable is True

    def test_format_network_errors_package_not_found_no_suggestions(self):
        """Test formatting PackageNotFoundError without suggestions."""
        errors = [
            {
                "type": "PackageNotFoundError",
                "package": "nonexistent-pkg",
                "message": "Package not found",
                "suggestions": [],
            }
        ]

        result = ErrorFormatter.format_network_errors(errors)

        assert len(result) == 1
        assert "Did you mean:" not in result[0].suggestion
        assert "Check the package name" in result[0].suggestion

    def test_format_network_errors_network_error(self):
        """Test formatting NetworkError."""
        errors = [
            {
                "type": "NetworkError",
                "package": "requests",
                "message": "Connection timeout",
                "retry_suggested": True,
            }
        ]

        result = ErrorFormatter.format_network_errors(errors)

        assert len(result) == 1
        assert result[0].error_code == "network_error"
        assert "requests" in result[0].message
        assert "Connection timeout" in result[0].message
        assert "retried automatically" in result[0].suggestion
        assert result[0].severity == ErrorSeverity.ERROR

    def test_format_network_errors_network_error_no_retry(self):
        """Test formatting NetworkError without retry suggestion."""
        errors = [
            {
                "type": "NetworkError",
                "package": "requests",
                "message": "Connection failed",
                "retry_suggested": False,
            }
        ]

        result = ErrorFormatter.format_network_errors(errors)

        assert len(result) == 1
        assert "retried automatically" not in result[0].suggestion
        assert "internet connection" in result[0].suggestion

    def test_format_network_errors_generic_error(self):
        """Test formatting generic error type."""
        errors = [
            {
                "type": "UnknownError",
                "package": "test-pkg",
                "message": "Something went wrong",
            }
        ]

        result = ErrorFormatter.format_network_errors(errors)

        assert len(result) == 1
        assert result[0].error_code == "generic_error"
        assert "test-pkg" in result[0].message
        assert "Something went wrong" in result[0].message
        assert result[0].context["type"] == "UnknownError"


class TestExceptionFormatting:
    """Test exception formatting."""

    def test_format_exception_project_parsing_error(self):
        """Test formatting ProjectParsingError."""
        exc = ProjectParsingError("Invalid TOML", Path("/test/pyproject.toml"))

        result = ErrorFormatter.format_exception(exc)

        assert result.error_code == "project_parse_error"
        assert "Invalid TOML" in result.message
        assert result.severity == ErrorSeverity.CRITICAL
        assert result.recoverable is False
        assert result.context["file_path"] == "/test/pyproject.toml"

    def test_format_exception_project_parsing_error_no_path(self):
        """Test formatting ProjectParsingError without file path."""
        exc = ProjectParsingError("Invalid TOML", None)

        result = ErrorFormatter.format_exception(exc)

        assert result.context["file_path"] is None

    def test_format_exception_package_not_found_error(self):
        """Test formatting PackageNotFoundError."""
        exc = PackageNotFoundError("Package not found")
        context = {"package": "requests"}

        result = ErrorFormatter.format_exception(exc, context)

        assert result.error_code == "package_not_found"
        assert "requests" in result.message
        assert result.severity == ErrorSeverity.ERROR
        assert result.recoverable is True
        assert result.context == context

    def test_format_exception_package_not_found_error_no_context(self):
        """Test formatting PackageNotFoundError without context."""
        exc = PackageNotFoundError("Package not found")

        result = ErrorFormatter.format_exception(exc)

        assert "unknown" in result.message

    def test_format_exception_network_error(self):
        """Test formatting NetworkError."""
        exc = NetworkError("Connection failed")
        context = {"package": "httpx"}

        result = ErrorFormatter.format_exception(exc, context)

        assert result.error_code == "network_error"
        assert "Connection failed" in result.message
        assert result.severity == ErrorSeverity.ERROR
        assert result.recoverable is True
        assert "retried automatically" in result.suggestion

    def test_format_exception_generic_exception(self):
        """Test formatting generic exception."""
        exc = ValueError("Something went wrong")
        context = {"operation": "test"}

        result = ErrorFormatter.format_exception(exc, context)

        assert result.error_code == "unexpected_error"
        assert "Something went wrong" in result.message
        assert result.severity == ErrorSeverity.CRITICAL
        assert result.recoverable is False
        assert result.context["exception_type"] == "ValueError"

    def test_format_exception_no_context(self):
        """Test formatting exception without context."""
        exc = RuntimeError("Runtime issue")

        result = ErrorFormatter.format_exception(exc)

        assert result.error_code == "unexpected_error"
        assert "Runtime issue" in result.message
        assert result.context["exception_type"] == "RuntimeError"


class TestResponseFormatter:
    """Test ResponseFormatter functionality."""

    def test_format_scan_response_success(self, mocker):
        """Test formatting successful scan response."""
        # Mock scan result
        mock_result = mocker.Mock()
        mock_result.dependencies = [mocker.Mock(), mocker.Mock()]  # 2 dependencies
        mock_result.dependencies[0].model_dump.return_value = {
            "name": "requests",
            "version": ">=2.0.0",
        }
        mock_result.dependencies[1].model_dump.return_value = {
            "name": "httpx",
            "version": ">=0.24.0",
        }
        mock_result.failed_deps = []
        mock_result.successful_deps = 2
        mock_result.partial_success = False
        mock_result.project_name = "test-project"
        mock_result.project_path = Path("/test/project")
        mock_result.warnings = []
        mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0)

        result = ResponseFormatter.format_scan_response(mock_result)

        assert result["success"] is True
        assert result["partial_success"] is False
        assert result["project_name"] == "test-project"
        assert result["project_path"] == "/test/project"
        assert len(result["dependencies"]) == 2
        assert result["dependency_count"] == 2
        assert len(result["errors"]) == 0
        assert result["scan_timestamp"] == "2025-01-08T12:00:00"

    def test_format_scan_response_with_errors(self, mocker):
        """Test formatting scan response with dependency errors."""
        # Mock scan result with failures
        mock_result = mocker.Mock()
        mock_result.dependencies = [mocker.Mock()]  # 1 successful dependency
        mock_result.dependencies[0].model_dump.return_value = {
            "name": "requests",
            "version": ">=2.0.0",
        }
        mock_result.failed_deps = [
            {
                "dependency_string": "bad@pkg",
                "error": "Invalid package name format: 'bad@pkg'",
                "source": "dependencies",
            }
        ]
        mock_result.successful_deps = 1
        mock_result.partial_success = True
        mock_result.project_name = "test-project"
        mock_result.project_path = Path("/test/project")
        mock_result.warnings = ["Some warning"]
        mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0)

        result = ResponseFormatter.format_scan_response(mock_result)

        assert result["success"] is True  # Has some dependencies
        assert result["partial_success"] is True
        assert len(result["dependencies"]) == 1
        assert len(result["errors"]) == 1
        assert result["errors"][0]["code"] == "invalid_package_name"
        assert result["errors"][0]["severity"] == "warning"
        assert len(result["suggestions"]) == 1

    def test_format_scan_response_no_dependencies(self, mocker):
        """Test formatting scan response with no successful dependencies."""
        # Mock scan result with no successful dependencies
        mock_result = mocker.Mock()
        mock_result.dependencies = []
        mock_result.failed_deps = [
            {
                "dependency_string": "",
                "error": "Empty dependency string",
                "source": "dependencies",
            }
        ]
        mock_result.successful_deps = 0
        mock_result.partial_success = False
        mock_result.project_name = "test-project"
        mock_result.project_path = Path("/test/project")
        mock_result.warnings = []
        mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0)

        result = ResponseFormatter.format_scan_response(mock_result)

        assert result["success"] is False
        assert result["dependency_count"] == 0
        assert len(result["errors"]) == 1
        assert result["errors"][0]["code"] == "empty_dependency"

    def test_format_scan_response_error_structure(self, mocker):
        """Test the structure of formatted errors in scan response."""
        # Mock scan result with multiple error types
        mock_result = mocker.Mock()
        mock_result.dependencies = []
        mock_result.failed_deps = [
            {
                "dependency_string": "bad@pkg",
                "error": "Invalid package name format: 'bad@pkg'",
                "source": "dependencies",
            },
            {
                "dependency_string": "pkg[unclosed",
                "error": "Unclosed bracket in extras: 'pkg[unclosed'",
                "source": "dev-dependencies",
            },
        ]
        mock_result.successful_deps = 0
        mock_result.partial_success = False
        mock_result.project_name = "test-project"
        mock_result.project_path = Path("/test/project")
        mock_result.warnings = []
        mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0)

        result = ResponseFormatter.format_scan_response(mock_result)

        # Check error structure
        assert len(result["errors"]) == 2

        error1 = result["errors"][0]
        assert "message" in error1
        assert "suggestion" in error1
        assert "severity" in error1
        assert "code" in error1
        assert "recoverable" in error1

        # Check suggestions are collected
        assert len(result["suggestions"]) == 2
        assert all(suggestion for suggestion in result["suggestions"])


class TestEdgeCases:
    """Test edge cases and error conditions."""

    def test_format_dependency_parsing_errors_missing_keys(self):
        """Test handling of malformed failure dictionaries."""
        failed_deps = [
            {
                # Missing some keys
                "dependency_string": "test-pkg",
                # "error" key missing
                "source": "dependencies",
            }
        ]

        result = ErrorFormatter.format_dependency_parsing_errors(failed_deps)

        assert len(result) == 1
        # Should handle gracefully with defaults
        assert result[0].error_code == "dependency_parse_unknown"

    def test_format_network_errors_missing_keys(self):
        """Test handling of malformed network error dictionaries."""
        errors = [
            {
                "type": "NetworkError",
                # Missing "package" and "message" keys
            }
        ]

        result = ErrorFormatter.format_network_errors(errors)

        assert len(result) == 1
        assert "unknown" in result[0].message
        assert result[0].error_code == "network_error"

    def test_pattern_matching_edge_cases(self):
        """Test regex pattern matching with edge cases."""
        # Test with pattern that might not match
        result = ErrorFormatter._format_single_dependency_error(
            "test", "Some completely different error message", "dependencies"
        )

        assert result.error_code == "dependency_parse_unknown"
        assert result.recoverable is True

    def test_suggestion_generation_edge_cases(self):
        """Test suggestion generation with edge cases."""
        # Empty string
        result = ErrorFormatter._suggest_package_name_fix("")
        assert result == "package-name"

        # Only special characters
        result = ErrorFormatter._suggest_package_name_fix("@#$%")
        assert result == "package-name"

        # Very long string
        long_name = "a" * 200 + "@" + "b" * 200
        result = ErrorFormatter._suggest_package_name_fix(long_name)
        assert "@" not in result
        assert "-" in result
