"""
Unit tests for handler signature validation.

Tests:
- Args dict pattern validation
- Typed parameters pattern validation
- Type compatibility checking
- Required vs optional field handling
- Return type validation
- Integration with ToolRegistry
"""

import pytest
from typing import Optional, List, Dict, Any, Union

from memrail.tools import (
    ToolRegistry,
    SignatureValidationError,
    SignatureValidationResult,
    HandlerSignatureValidator,
    validate_handler_signature,
)


class TestSignatureValidationResult:
    """Tests for SignatureValidationResult model."""

    def test_valid_result(self):
        """Test creating a valid result."""
        result = SignatureValidationResult(
            is_valid=True,
            handler_pattern="args_dict",
        )

        assert result.is_valid
        assert len(result.errors) == 0
        assert result.handler_pattern == "args_dict"

    def test_invalid_result(self):
        """Test creating an invalid result."""
        result = SignatureValidationResult(
            is_valid=False,
            errors=[
                SignatureValidationError(field="to", message="Missing required field")
            ],
        )

        assert not result.is_valid
        assert len(result.errors) == 1
        assert result.errors[0].field == "to"

    def test_raise_if_invalid(self):
        """Test raise_if_invalid method."""
        result = SignatureValidationResult(
            is_valid=False,
            errors=[
                SignatureValidationError(field="to", message="Missing required field")
            ],
        )

        with pytest.raises(TypeError, match="Handler signature mismatch"):
            result.raise_if_invalid("send_email")

    def test_raise_if_invalid_passes_when_valid(self):
        """Test that raise_if_invalid doesn't raise when valid."""
        result = SignatureValidationResult(is_valid=True)

        # Should not raise
        result.raise_if_invalid("test_tool")


class TestArgsDictPattern:
    """Tests for the standard args dict pattern: async def handler(args: dict)"""

    def test_args_dict_pattern_with_annotation(self):
        """Test validation with args: dict annotation."""
        async def handler(args: dict) -> dict:
            return {"success": True}

        schema = {"to": str, "subject": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid
        assert result.handler_pattern == "args_dict"

    def test_args_dict_pattern_without_annotation(self):
        """Test validation without type annotation (common pattern)."""
        async def handler(args):
            return {"success": True}

        schema = {"to": str, "subject": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid
        assert result.handler_pattern == "args_dict"

    def test_args_dict_with_dict_subtype(self):
        """Test with Dict[str, Any] annotation."""
        async def handler(args: Dict[str, Any]) -> dict:
            return {"success": True}

        schema = {"to": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid
        assert result.handler_pattern == "args_dict"

    def test_single_dict_param_different_name(self):
        """Test with single dict param but different name."""
        async def handler(data: dict) -> dict:
            return {"success": True}

        schema = {"to": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid
        assert result.handler_pattern == "args_dict"

    def test_args_param_wrong_type_annotation(self):
        """Test error when args param has wrong type annotation.

        When someone writes `args: str`, it's treated as typed params pattern
        (not args dict pattern) since they explicitly annotated a non-dict type.
        This results in validation failing due to schema field mismatch.
        """
        async def handler(args: str) -> dict:
            return {"success": True}

        schema = {"to": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert not result.is_valid
        # Treated as typed_params - 'to' is missing and 'args' is not in schema
        assert result.handler_pattern == "typed_params"
        assert any("to" in e.field or "args" in e.field for e in result.errors)


class TestTypedParamsPattern:
    """Tests for the typed parameters pattern: async def handler(to: str, subject: str)"""

    def test_typed_params_exact_match(self):
        """Test validation when params exactly match schema."""
        async def handler(to: str, subject: str) -> dict:
            return {"sent": True}

        schema = {"to": str, "subject": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid
        assert result.handler_pattern == "typed_params"

    def test_typed_params_with_optional(self):
        """Test validation with optional params matching optional schema fields."""
        async def handler(to: str, subject: str, cc: Optional[str] = None) -> dict:
            return {"sent": True}

        schema = {"to": str, "subject": str, "cc": Optional[str]}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_typed_params_missing_required(self):
        """Test error when required schema field is missing from params."""
        async def handler(to: str) -> dict:
            return {"sent": True}

        schema = {"to": str, "subject": str, "body": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert not result.is_valid
        assert any("subject" in e.field for e in result.errors)
        assert any("body" in e.field for e in result.errors)

    def test_typed_params_with_kwargs(self):
        """Test that **kwargs allows missing params."""
        async def handler(to: str, **kwargs) -> dict:
            return {"sent": True}

        schema = {"to": str, "subject": str, "body": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_typed_params_extra_params_warning(self):
        """Test warning when handler has params not in schema."""
        async def handler(to: str, extra_param: str) -> dict:
            return {"sent": True}

        schema = {"to": str}
        result = validate_handler_signature(handler, schema, "test_tool", strict=False)

        assert result.is_valid  # Valid in non-strict mode
        assert any("extra_param" in w.field for w in result.warnings)

    def test_typed_params_extra_params_error_strict(self):
        """Test error when handler has params not in schema in strict mode."""
        async def handler(to: str, extra_param: str) -> dict:
            return {"sent": True}

        schema = {"to": str}
        result = validate_handler_signature(handler, schema, "test_tool", strict=True)

        assert not result.is_valid
        assert any("extra_param" in e.field for e in result.errors)


class TestTypeCompatibility:
    """Tests for type compatibility checking."""

    def test_string_type_match(self):
        """Test string type validation."""
        async def handler(name: str) -> dict:
            return {}

        schema = {"name": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_integer_type_match(self):
        """Test integer type validation."""
        async def handler(count: int) -> dict:
            return {}

        schema = {"count": int}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_number_accepts_int_and_float(self):
        """Test that JSON number type accepts both int and float."""
        async def handler(value: float) -> dict:
            return {}

        # JSON Schema number type
        schema = {"type": "object", "properties": {"value": {"type": "number"}}, "required": ["value"]}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_type_mismatch_error(self):
        """Test error on type mismatch."""
        async def handler(name: int) -> dict:  # Should be str
            return {}

        schema = {"name": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert not result.is_valid
        assert any("type mismatch" in e.message.lower() for e in result.errors)

    def test_list_type_match(self):
        """Test list type validation."""
        async def handler(items: list) -> dict:
            return {}

        schema = {"items": list}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_typed_list_match(self):
        """Test List[str] type validation."""
        async def handler(items: List[str]) -> dict:
            return {}

        schema = {"items": list}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid


class TestJSONSchemaFormat:
    """Tests for JSON Schema format input."""

    def test_json_schema_object_type(self):
        """Test validation against JSON Schema format."""
        async def handler(to: str, subject: str, body: str) -> dict:
            return {}

        schema = {
            "type": "object",
            "properties": {
                "to": {"type": "string"},
                "subject": {"type": "string"},
                "body": {"type": "string"},
            },
            "required": ["to", "subject", "body"],
        }

        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_json_schema_optional_field(self):
        """Test JSON Schema with optional field (not in required)."""
        async def handler(to: str, cc: Optional[str] = None) -> dict:
            return {}

        schema = {
            "type": "object",
            "properties": {
                "to": {"type": "string"},
                "cc": {"type": "string"},
            },
            "required": ["to"],  # cc is optional
        }

        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid


class TestReturnTypeValidation:
    """Tests for handler return type validation."""

    def test_return_dict_valid(self):
        """Test that return type dict is valid."""
        async def handler(args: dict) -> dict:
            return {}

        result = validate_handler_signature(handler, {"x": str}, "test_tool")

        assert result.is_valid

    def test_return_dict_subtype_valid(self):
        """Test that Dict[str, Any] return type is valid."""
        async def handler(args: dict) -> Dict[str, Any]:
            return {}

        result = validate_handler_signature(handler, {"x": str}, "test_tool")

        assert result.is_valid

    def test_return_any_valid(self):
        """Test that Any return type is valid."""
        async def handler(args: dict) -> Any:
            return {}

        result = validate_handler_signature(handler, {"x": str}, "test_tool")

        assert result.is_valid

    def test_return_none_invalid(self):
        """Test that None return type is invalid."""
        async def handler(args: dict) -> None:
            pass

        result = validate_handler_signature(handler, {"x": str}, "test_tool")

        assert not result.is_valid
        assert any("return" in e.field for e in result.errors)

    def test_no_return_annotation_valid(self):
        """Test that missing return annotation is valid."""
        async def handler(args: dict):
            return {}

        result = validate_handler_signature(handler, {"x": str}, "test_tool")

        assert result.is_valid


class TestRegistryIntegration:
    """Tests for integration with ToolRegistry."""

    def test_decorator_valid_args_dict(self):
        """Test decorator with valid args dict pattern."""
        registry = ToolRegistry(validate_signatures=True)

        @registry.tool("test", "Test tool", {"to": str, "subject": str}, ["test_project"])
        async def handler(args: dict) -> dict:
            return {"success": True}

        tool = registry.get_tool("test")
        assert tool is not None
        assert tool.has_handler

    def test_decorator_valid_typed_params(self):
        """Test decorator with valid typed params pattern."""
        registry = ToolRegistry(validate_signatures=True)

        @registry.tool("test", "Test tool", {"to": str, "subject": str}, ["test_project"])
        async def handler(to: str, subject: str) -> dict:
            return {"success": True}

        tool = registry.get_tool("test")
        assert tool is not None

    def test_decorator_invalid_missing_params(self):
        """Test decorator raises on missing required params."""
        registry = ToolRegistry(validate_signatures=True)

        with pytest.raises(TypeError, match="Handler signature mismatch"):
            @registry.tool("test", "Test tool", {"to": str, "subject": str, "body": str}, ["test_project"])
            async def handler(to: str) -> dict:
                return {"success": True}

    def test_decorator_validation_disabled(self):
        """Test that validation can be disabled."""
        registry = ToolRegistry(validate_signatures=False)

        # This would fail validation but should pass with validation disabled
        @registry.tool("test", "Test tool", {"to": str, "subject": str, "body": str}, ["test_project"])
        async def handler(to: str) -> dict:
            return {"success": True}

        tool = registry.get_tool("test")
        assert tool is not None

    def test_register_tool_valid(self):
        """Test programmatic registration with valid handler."""
        registry = ToolRegistry(validate_signatures=True)

        async def handler(args: dict) -> dict:
            return {"success": True}

        registry.register_tool(
            name="test",
            description="Test tool",
            schema={"to": str},
            projects=["test_project"],
            handler=handler,
        )

        tool = registry.get_tool("test")
        assert tool is not None

    def test_register_tool_invalid(self):
        """Test programmatic registration raises on invalid handler."""
        registry = ToolRegistry(validate_signatures=True)

        async def handler(to: str) -> dict:  # Missing subject and body
            return {"success": True}

        with pytest.raises(TypeError, match="Handler signature mismatch"):
            registry.register_tool(
                name="test",
                description="Test tool",
                schema={"to": str, "subject": str, "body": str},
                projects=["test_project"],
                handler=handler,
            )

    def test_register_tool_without_handler_skips_validation(self):
        """Test that registration without handler skips validation."""
        registry = ToolRegistry(validate_signatures=True)

        # Should not raise even though schema has fields
        registry.register_tool(
            name="test",
            description="Test tool",
            schema={"to": str, "subject": str},
            projects=["test_project"],
            handler=None,
        )

        tool = registry.get_tool("test")
        assert tool is not None
        assert not tool.has_handler

    def test_strict_validation_mode(self):
        """Test strict validation mode treats warnings as errors."""
        registry = ToolRegistry(validate_signatures=True, strict_validation=True)

        with pytest.raises(TypeError, match="Handler signature mismatch"):
            @registry.tool("test", "Test tool", {"to": str}, ["test_project"])
            async def handler(to: str, extra_param: str) -> dict:
                return {"success": True}

    def test_non_strict_allows_extra_params(self):
        """Test non-strict mode allows extra params with warning."""
        registry = ToolRegistry(validate_signatures=True, strict_validation=False)

        # Should not raise - extra_param is just a warning
        @registry.tool("test", "Test tool", {"to": str}, ["test_project"])
        async def handler(to: str, extra_param: str = "default") -> dict:
            return {"success": True}

        tool = registry.get_tool("test")
        assert tool is not None


class TestEdgeCases:
    """Tests for edge cases and special scenarios."""

    def test_empty_schema(self):
        """Test validation with empty schema."""
        async def handler(args: dict) -> dict:
            return {}

        schema = {}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_no_params_handler(self):
        """Test error when handler has no parameters."""
        async def handler() -> dict:
            return {}

        schema = {"x": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert not result.is_valid

    def test_var_positional_args(self):
        """Test handler with *args is handled."""
        async def handler(*args) -> dict:
            return {}

        schema = {"x": str}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert not result.is_valid

    def test_union_type_in_schema(self):
        """Test schema with Union type."""
        async def handler(value: Union[str, int]) -> dict:
            return {}

        schema = {"value": Union[str, int]}
        result = validate_handler_signature(handler, schema, "test_tool")

        assert result.is_valid

    def test_unannotated_typed_params_warning(self):
        """Test warning when typed params have no annotations."""
        async def handler(to, subject) -> dict:
            return {}

        schema = {"to": str, "subject": str}
        result = validate_handler_signature(handler, schema, "test_tool", strict=False)

        assert result.is_valid
        assert len(result.warnings) >= 2  # Warnings for missing annotations


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
