# -*- coding: utf-8 -*-
"""Location: ./tests/fuzz/test_jsonrpc_fuzz.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Property-based fuzz testing for JSON-RPC validation.
"""

# Standard
import json

# Third-Party
from hypothesis import example, given
from hypothesis import strategies as st
import pytest

# First-Party
from mcpgateway.validation.jsonrpc import JSONRPCError, validate_request, validate_response


class TestJSONRPCRequestFuzzing:
    """Fuzz testing for JSON-RPC request validation."""

    @given(st.binary())
    @example(b"")  # Empty binary
    @example(b"\x00\x01\x02")  # Non-UTF8 bytes
    @example(b"\xff\xfe")  # BOM markers
    def test_validate_request_handles_binary_input(self, raw_bytes):
        """Test that binary input never crashes the validator."""
        try:
            # First try to decode as UTF-8, then parse as JSON
            text = raw_bytes.decode("utf-8", errors="ignore")
            data = json.loads(text)
            # Only validate if we get a dict (JSON-RPC expects dict)
            if isinstance(data, dict):
                validate_request(data)
        except (JSONRPCError, ValueError, TypeError, UnicodeDecodeError, json.JSONDecodeError, AttributeError):
            # These are acceptable exceptions
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.text())
    @example("")  # Empty string
    @example("null")  # Valid JSON but invalid request
    @example('{"incomplete":')  # Malformed JSON
    @example('{"jsonrpc": "2.0"}')  # Missing method
    def test_validate_request_handles_text_input(self, text_input):
        """Test that text input never crashes the validator."""
        try:
            data = json.loads(text_input)
            # Only validate if we get a dict (JSON-RPC expects dict)
            if isinstance(data, dict):
                validate_request(data)
        except (JSONRPCError, ValueError, TypeError, json.JSONDecodeError, AttributeError):
            # Expected exceptions for invalid input
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(
        st.dictionaries(
            keys=st.text(min_size=1, max_size=50),
            values=st.recursive(st.one_of(st.none(), st.booleans(), st.integers(), st.floats(), st.text()), lambda children: st.lists(children) | st.dictionaries(st.text(), children), max_leaves=20),
            max_size=20,
        )
    )
    def test_validate_request_handles_arbitrary_dicts(self, data):
        """Test arbitrary dictionary structures."""
        try:
            validate_request(data)
        except (JSONRPCError, ValueError, TypeError):
            # Expected exceptions for invalid structures
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.text(min_size=0, max_size=10))
    def test_jsonrpc_version_field_fuzzing(self, version):
        """Test jsonrpc version field with various inputs."""
        request = {"jsonrpc": version, "method": "test", "id": 1}
        try:
            validate_request(request)
            # If validation succeeds, it should be version "2.0"
            assert version == "2.0"
        except JSONRPCError:
            # Expected for non-"2.0" versions
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.one_of(st.text(min_size=0, max_size=100), st.integers(), st.floats(), st.booleans(), st.none(), st.lists(st.text()), st.dictionaries(st.text(), st.text())))
    def test_method_field_fuzzing(self, method):
        """Test method field with various data types."""
        request = {"jsonrpc": "2.0", "method": method, "id": 1}
        try:
            validate_request(request)
            # If validation succeeds, method should be non-empty string
            assert isinstance(method, str) and len(method) > 0
        except JSONRPCError:
            # Expected for invalid methods
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.one_of(st.integers(), st.text(min_size=0, max_size=100), st.booleans(), st.floats(), st.none(), st.lists(st.integers()), st.dictionaries(st.text(), st.text())))
    def test_id_field_fuzzing(self, request_id):
        """Test ID field with various data types."""
        request = {"jsonrpc": "2.0", "method": "test", "id": request_id}
        try:
            validate_request(request)
            # If validation succeeds, ID should be string or int (not bool)
            assert isinstance(request_id, (str, int)) and not isinstance(request_id, bool)
        except JSONRPCError:
            # Expected for invalid IDs
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.one_of(st.lists(st.integers()), st.dictionaries(st.text(), st.text()), st.text(), st.integers(), st.booleans(), st.none()))
    def test_params_field_fuzzing(self, params):
        """Test params field with various data types."""
        request = {"jsonrpc": "2.0", "method": "test", "params": params, "id": 1}
        try:
            validate_request(request)
            # If validation succeeds, params should be dict, list, or None
            assert isinstance(params, (dict, list, type(None)))
        except JSONRPCError:
            # Expected for invalid params
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.dictionaries(keys=st.text(min_size=1, max_size=20), values=st.one_of(st.text(), st.integers(), st.booleans(), st.none()), min_size=0, max_size=10))
    def test_extra_fields_fuzzing(self, extra_fields):
        """Test requests with extra fields."""
        request = {"jsonrpc": "2.0", "method": "test", "id": 1, **extra_fields}
        try:
            validate_request(request)
            # Should succeed regardless of extra fields
        except JSONRPCError:
            # Should only fail for core field validation issues
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")


class TestJSONRPCResponseFuzzing:
    """Fuzz testing for JSON-RPC response validation."""

    @given(
        st.dictionaries(
            keys=st.text(min_size=1, max_size=50),
            values=st.recursive(st.one_of(st.none(), st.booleans(), st.integers(), st.floats(), st.text()), lambda children: st.lists(children) | st.dictionaries(st.text(), children), max_leaves=20),
            max_size=20,
        )
    )
    def test_validate_response_handles_arbitrary_dicts(self, data):
        """Test response validation with arbitrary dictionary structures."""
        try:
            validate_response(data)
        except (JSONRPCError, ValueError, TypeError):
            # Expected exceptions for invalid structures
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.one_of(st.integers(), st.text(min_size=0, max_size=100), st.booleans(), st.floats(), st.none(), st.lists(st.integers()), st.dictionaries(st.text(), st.text())))
    def test_response_id_field_fuzzing(self, response_id):
        """Test response ID field with various data types."""
        response = {"jsonrpc": "2.0", "result": "success", "id": response_id}
        try:
            validate_response(response)
            # If validation succeeds, ID should be string, int, or None (not bool)
            assert isinstance(response_id, (str, int, type(None))) and not isinstance(response_id, bool)
        except JSONRPCError:
            # Expected for invalid IDs
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.one_of(st.text(), st.integers(), st.booleans(), st.none(), st.lists(st.text()), st.dictionaries(st.text(), st.text())))
    def test_result_field_fuzzing(self, result):
        """Test result field with various data types."""
        response = {"jsonrpc": "2.0", "result": result, "id": 1}
        try:
            validate_response(response)
            # Should succeed with any result type
        except JSONRPCError:
            # Should not fail due to result content
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(
        st.one_of(
            st.dictionaries(keys=st.sampled_from(["code", "message", "data"]), values=st.one_of(st.integers(), st.text(), st.booleans()), min_size=1, max_size=3),
            st.text(),
            st.integers(),
            st.booleans(),
            st.none(),
            st.lists(st.text()),
        )
    )
    def test_error_field_fuzzing(self, error):
        """Test error field with various structures."""
        response = {"jsonrpc": "2.0", "error": error, "id": 1}
        try:
            validate_response(response)
            # If validation succeeds, error should be proper dict with code/message
            if isinstance(error, dict):
                assert "code" in error and "message" in error
                assert isinstance(error["code"], int)
                assert isinstance(error["message"], str)
        except JSONRPCError:
            # Expected for invalid error structures
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    def test_response_missing_required_fields(self):
        """Test responses missing required result/error fields."""
        response = {"jsonrpc": "2.0", "id": 1}
        try:
            validate_response(response)
            pytest.fail("Should have failed validation")
        except JSONRPCError:
            # Expected
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    def test_response_both_result_and_error(self):
        """Test responses with both result and error fields."""
        response = {"jsonrpc": "2.0", "result": "success", "error": {"code": -1, "message": "error"}, "id": 1}
        try:
            validate_response(response)
            pytest.fail("Should have failed validation")
        except JSONRPCError:
            # Expected
            pass
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")


class TestJSONRPCErrorFuzzing:
    """Fuzz testing for JSONRPCError class."""

    @given(st.integers(), st.text(min_size=0, max_size=200))
    def test_jsonrpc_error_creation(self, code, message):
        """Test JSONRPCError creation with various inputs."""
        try:
            error = JSONRPCError(code, message)
            assert error.code == code
            assert error.message == message
            # Should be able to convert to dict
            error_dict = error.to_dict()
            assert isinstance(error_dict, dict)
            assert error_dict["jsonrpc"] == "2.0"
        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")

    @given(st.integers(), st.text(min_size=0, max_size=200), st.one_of(st.none(), st.text(), st.integers(), st.dictionaries(st.text(), st.text())), st.one_of(st.none(), st.integers(), st.text()))
    def test_jsonrpc_error_with_data_and_id(self, code, message, data, request_id):
        """Test JSONRPCError with optional data and request_id."""
        try:
            error = JSONRPCError(code, message, data, request_id)
            assert error.code == code
            assert error.message == message
            assert error.data == data
            assert error.request_id == request_id

            # Should be able to convert to dict
            error_dict = error.to_dict()
            assert isinstance(error_dict, dict)
            assert error_dict["jsonrpc"] == "2.0"
            assert error_dict["error"]["code"] == code
            assert error_dict["error"]["message"] == message
            assert error_dict["request_id"] == request_id

            if data is not None:
                assert error_dict["error"]["data"] == data
            else:
                assert "data" not in error_dict["error"]

        except Exception as e:
            pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")
