"""
Tests for OpenAI integration validation.

FUNDAMENTAL PRINCIPLE:
- AI provides input (OpenAI function call responses)
- APE parses, validates, and decides
- AI never executes logic directly

These tests verify that:
1. OpenAI output is treated as untrusted input
2. All validation happens in APE runtime
3. Invalid/hallucinated AI output is rejected deterministically
"""

import pytest
from unittest.mock import Mock
import json

from ape_openai.executor import execute_openai_call


# ============================================================================
# POSITIVE TESTS - Expected Valid Use
# ============================================================================


class TestValidOpenAIInput:
    """Test that valid OpenAI function call responses are accepted."""

    def test_valid_parameters_accepted(self):
        """AI output with correct parameters passes APE validation."""
        # Mock APE module
        mock_signature = Mock()
        mock_signature.inputs = {"a": "int", "b": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        mock_module.call.return_value = 8

        # OpenAI response: valid parameters
        openai_arguments = {"a": 5, "b": 3}

        result = execute_openai_call(mock_module, "add", openai_arguments)

        assert result == 8
        mock_module.call.assert_called_once_with("add", a=5, b=3)

    def test_json_string_arguments_parsed(self):
        """AI output as JSON string is parsed before validation."""
        mock_signature = Mock()
        mock_signature.inputs = {"x": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        mock_module.call.return_value = 25

        # OpenAI sends JSON string
        openai_json = '{"x": 5}'

        result = execute_openai_call(mock_module, "square", openai_json)

        assert result == 25
        mock_module.call.assert_called_once_with("square", x=5)

    def test_empty_parameters_accepted(self):
        """AI output with no parameters works for zero-arg functions."""
        mock_signature = Mock()
        mock_signature.inputs = {}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        mock_module.call.return_value = "2025-12-07T10:30:00Z"

        openai_arguments = {}

        result = execute_openai_call(mock_module, "get_timestamp", openai_arguments)

        assert result == "2025-12-07T10:30:00Z"

    def test_all_required_parameters_provided(self):
        """AI output with all required fields passes validation."""
        mock_signature = Mock()
        mock_signature.inputs = {
            "amount": "float",
            "rate": "float",
            "years": "int"
        }

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        mock_module.call.return_value = 1276.28

        openai_arguments = {
            "amount": 1000.0,
            "rate": 0.05,
            "years": 5
        }

        result = execute_openai_call(
            mock_module,
            "calculate_compound_interest",
            openai_arguments
        )

        assert result == 1276.28


# ============================================================================
# NEGATIVE TESTS - Invalid/Hallucinated AI Output
# ============================================================================


class TestOpenAIHallucinationsRejected:
    """
    Test that hallucinated/invalid OpenAI output is rejected.
    
    CRITICAL: These tests prove APE has decision authority.
    AI cannot bypass validation by sending creative/incorrect data.
    """

    def test_missing_required_parameter_rejected(self):
        """AI hallucination: missing required field → APE rejects."""
        mock_signature = Mock()
        mock_signature.inputs = {"a": "int", "b": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # OpenAI hallucinates: forgets parameter 'b'
        hallucinated_arguments = {"a": 5}

        with pytest.raises(TypeError) as exc_info:
            execute_openai_call(mock_module, "add", hallucinated_arguments)

        assert "Missing required arguments" in str(exc_info.value)
        assert "{'b'}" in str(exc_info.value)

    def test_extra_parameter_rejected(self):
        """AI hallucination: invents non-existent parameter → APE rejects."""
        mock_signature = Mock()
        mock_signature.inputs = {"x": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # OpenAI hallucinates: adds 'y' and 'z' that don't exist
        hallucinated_arguments = {"x": 5, "y": 10, "z": "extra"}

        with pytest.raises(TypeError) as exc_info:
            execute_openai_call(mock_module, "square", hallucinated_arguments)

        assert "Unknown arguments" in str(exc_info.value)
        assert "'y'" in str(exc_info.value) or "'z'" in str(exc_info.value)

    def test_hallucinated_function_name_rejected(self):
        """AI hallucination: calls non-existent function → APE rejects."""
        mock_module = Mock()
        mock_module.get_function_signature.side_effect = KeyError("Not found")
        mock_module.list_functions.return_value = ["add", "subtract"]

        # OpenAI hallucinates function name
        hallucinated_arguments = {"x": 5}

        with pytest.raises(KeyError) as exc_info:
            execute_openai_call(
                mock_module,
                "magical_compute_everything",
                hallucinated_arguments
            )

        assert "not found" in str(exc_info.value)
        assert "Available" in str(exc_info.value)

    def test_invalid_json_rejected(self):
        """AI hallucination: malformed JSON → APE rejects."""
        mock_signature = Mock()
        mock_signature.inputs = {"x": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # OpenAI sends broken JSON
        malformed_json = '{"x": 5, "y":}'

        with pytest.raises(ValueError) as exc_info:
            execute_openai_call(mock_module, "func", malformed_json)

        assert "Invalid JSON" in str(exc_info.value)

    def test_wrong_type_rejected(self):
        """AI hallucination: sends string instead of int → APE rejects."""
        mock_signature = Mock()
        mock_signature.inputs = {"count": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        # Simulate type error during execution
        mock_module.call.side_effect = TypeError("expected int, got str")

        # OpenAI sends wrong type
        hallucinated_arguments = {"count": "five"}

        with pytest.raises(Exception) as exc_info:
            execute_openai_call(mock_module, "repeat", hallucinated_arguments)

        # APE runtime catches type errors
        assert "failed" in str(exc_info.value).lower() or "expected int" in str(exc_info.value)

    def test_nested_structure_mismatch_rejected(self):
        """AI hallucination: wrong nested structure → APE rejects."""
        mock_signature = Mock()
        mock_signature.inputs = {"data": "dict"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # OpenAI sends list instead of dict
        hallucinated_arguments = {"data": [1, 2, 3]}

        # APE would catch this during runtime validation
        # For this test, we verify the structure is passed as-is
        mock_module.call.side_effect = TypeError("Expected dict, got list")

        with pytest.raises(Exception):
            execute_openai_call(mock_module, "process", hallucinated_arguments)

    def test_partial_truncated_payload_rejected(self):
        """AI hallucination: incomplete/truncated response → APE rejects."""
        mock_signature = Mock()
        mock_signature.inputs = {
            "field_a": "str",
            "field_b": "str",
            "field_c": "str"
        }

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # OpenAI truncates mid-response
        truncated_arguments = {
            "field_a": "value1",
            "field_b": "value2"
            # Missing field_c
        }

        with pytest.raises(TypeError) as exc_info:
            execute_openai_call(mock_module, "func", truncated_arguments)

        assert "Missing required arguments" in str(exc_info.value)

    def test_null_value_in_required_field_rejected(self):
        """AI hallucination: null/None in required field → APE validates."""
        mock_signature = Mock()
        mock_signature.inputs = {"value": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        mock_module.call.side_effect = TypeError("Cannot process None")

        # OpenAI sends null
        hallucinated_arguments = {"value": None}

        with pytest.raises(Exception):
            execute_openai_call(mock_module, "compute", hallucinated_arguments)


# ============================================================================
# DECISION AUTHORITY VERIFICATION
# ============================================================================


class TestAPEDecisionAuthority:
    """
    Verify that APE runtime has exclusive decision authority.
    
    AI output is ONLY input. APE makes ALL execution decisions.
    """

    def test_ai_cannot_bypass_validation(self):
        """AI cannot execute functions by sending 'creative' payloads."""
        mock_signature = Mock()
        mock_signature.inputs = {"x": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # AI tries to be clever: adds metadata
        creative_payload = {
            "x": 5,
            "_execute_anyway": True,
            "_bypass_validation": True,
            "_force": True
        }

        # APE rejects extra fields
        with pytest.raises(TypeError) as exc_info:
            execute_openai_call(mock_module, "func", creative_payload)

        assert "Unknown arguments" in str(exc_info.value)

    def test_ai_output_parsed_never_executed(self):
        """AI output goes through parsing, not direct execution."""
        mock_signature = Mock()
        mock_signature.inputs = {}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        mock_module.call.return_value = "result"

        # AI sends valid arguments
        ai_output = {}

        result = execute_openai_call(mock_module, "func", ai_output)

        # Verify APE runtime.call() was invoked (APE decides execution)
        mock_module.call.assert_called_once_with("func")
        assert result == "result"

    def test_ape_validates_before_execution(self):
        """Validation happens BEFORE any execution logic runs."""
        mock_signature = Mock()
        mock_signature.inputs = {"a": "int", "b": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # AI sends invalid (missing parameter)
        invalid_arguments = {"a": 5}

        with pytest.raises(TypeError):
            execute_openai_call(mock_module, "add", invalid_arguments)

        # Execution never called because validation failed first
        mock_module.call.assert_not_called()

    def test_deterministic_rejection_of_invalid_input(self):
        """Same invalid input always rejected (deterministic)."""
        mock_signature = Mock()
        mock_signature.inputs = {"x": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        invalid_arguments = {"x": 5, "wrong_field": 10}  # Has x, but extra field

        # Run 10 times - should fail identically every time
        for _ in range(10):
            with pytest.raises(TypeError) as exc_info:
                execute_openai_call(mock_module, "func", invalid_arguments)

            assert "Unknown arguments" in str(exc_info.value)

        # Execution never attempted
        mock_module.call.assert_not_called()


# ============================================================================
# NO RECOVERY / NO BEST EFFORT
# ============================================================================


class TestNoRecoveryNoBestEffort:
    """
    Verify that invalid AI output is REJECTED, not recovered.
    
    No heuristics, no retries, no silent fixes.
    Invalid input → structured error → stop.
    """

    def test_no_parameter_guessing(self):
        """Missing parameters are NOT guessed or defaulted."""
        mock_signature = Mock()
        mock_signature.inputs = {"a": "int", "b": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        # Missing 'b'
        incomplete_arguments = {"a": 5}

        with pytest.raises(TypeError) as exc_info:
            execute_openai_call(mock_module, "add", incomplete_arguments)

        # Error is explicit, no silent defaulting
        assert "Missing required arguments" in str(exc_info.value)

    def test_no_type_coercion_recovery(self):
        """Wrong types are NOT silently coerced."""
        mock_signature = Mock()
        mock_signature.inputs = {"x": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature
        mock_module.call.side_effect = TypeError("Cannot convert str to int")

        # Wrong type
        wrong_type_arguments = {"x": "not_a_number"}

        with pytest.raises(Exception) as exc_info:
            execute_openai_call(mock_module, "compute", wrong_type_arguments)

        # No silent conversion attempted
        assert "failed" in str(exc_info.value).lower() or "convert" in str(exc_info.value).lower()

    def test_structured_error_on_failure(self):
        """Failures produce structured errors with context."""
        mock_signature = Mock()
        mock_signature.inputs = {"a": "int", "b": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        invalid_arguments = {"a": 5, "b": 3, "extra": 10}  # All required + extra

        with pytest.raises(TypeError) as exc_info:
            execute_openai_call(mock_module, "add", invalid_arguments)

        error_message = str(exc_info.value)
        # Error contains useful context
        assert "Unknown arguments" in error_message
        assert "extra" in error_message

    def test_no_silent_failure(self):
        """Invalid input raises exception, never silent."""
        mock_signature = Mock()
        mock_signature.inputs = {"x": "int"}

        mock_module = Mock()
        mock_module.get_function_signature.return_value = mock_signature

        invalid_arguments = {}

        # Must raise, not return None or empty result
        with pytest.raises(TypeError):
            execute_openai_call(mock_module, "func", invalid_arguments)


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