# tests/test_openapi_toolkit.py
"""
Test suite for OpenAPIToolkit:
1. Prance-based inline schema refs
2. Form-urlencoded support
3. Single-operation optimization
"""
import pytest
import json
from parrot.tools.openapitoolkit import OpenAPIToolkit


# ==============================================================================
# Test Fixtures
# ==============================================================================

@pytest.fixture
def simple_spec_with_refs():
    """OpenAPI spec with internal $ref references."""
    return {
        "openapi": "3.0.0",
        "info": {"title": "Test API", "version": "1.0.0"},
        "servers": [{"url": "https://api.test.com"}],
        "paths": {
            "/users": {
                "get": {
                    "operationId": "listUsers",
                    "responses": {
                        "200": {
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "$ref": "#/components/schemas/UserList"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "components": {
            "schemas": {
                "UserList": {
                    "type": "object",
                    "properties": {
                        "users": {
                            "type": "array",
                            "items": {"$ref": "#/components/schemas/User"}
                        }
                    }
                },
                "User": {
                    "type": "object",
                    "properties": {
                        "id": {"type": "string"},
                        "name": {"type": "string"},
                        "address": {"$ref": "#/components/schemas/Address"}
                    }
                },
                "Address": {
                    "type": "object",
                    "properties": {
                        "street": {"type": "string"},
                        "city": {"type": "string"}
                    }
                }
            }
        }
    }


@pytest.fixture
def form_encoded_spec():
    """OpenAPI spec with form-urlencoded request body."""
    return {
        "openapi": "3.0.0",
        "info": {"title": "Auth API", "version": "1.0.0"},
        "servers": [{"url": "https://auth.test.com"}],
        "paths": {
            "/login": {
                "post": {
                    "operationId": "login",
                    "requestBody": {
                        "required": True,
                        "content": {
                            "application/x-www-form-urlencoded": {
                                "schema": {
                                    "type": "object",
                                    "required": ["username", "password"],
                                    "properties": {
                                        "username": {
                                            "type": "string",
                                            "description": "User's login name"
                                        },
                                        "password": {
                                            "type": "string",
                                            "description": "User's password"
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }


@pytest.fixture
def json_and_form_spec():
    """OpenAPI spec with both JSON and form-urlencoded."""
    return {
        "openapi": "3.0.0",
        "info": {"title": "Multi-Content API", "version": "1.0.0"},
        "servers": [{"url": "https://api.test.com"}],
        "paths": {
            "/data": {
                "post": {
                    "operationId": "submitData",
                    "requestBody": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {"type": "string"}
                                    }
                                }
                            },
                            "application/x-www-form-urlencoded": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {"type": "string"}
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }


@pytest.fixture
def single_operation_spec():
    """OpenAPI spec with only one operation."""
    return {
        "openapi": "3.0.0",
        "info": {"title": "Single Op API", "version": "1.0.0"},
        "servers": [{"url": "https://api.test.com"}],
        "paths": {
            "/users/{userId}": {
                "get": {
                    "operationId": "getUser",
                    "parameters": [
                        {
                            "name": "userId",
                            "in": "path",
                            "required": True,
                            "schema": {"type": "string"},
                            "description": "User ID"
                        },
                        {
                            "name": "include_details",
                            "in": "query",
                            "required": False,
                            "schema": {"type": "boolean"},
                            "description": "Include detailed info"
                        }
                    ]
                }
            }
        }
    }


@pytest.fixture
def multi_operation_spec():
    """OpenAPI spec with multiple operations."""
    return {
        "openapi": "3.0.0",
        "info": {"title": "Multi Op API", "version": "1.0.0"},
        "servers": [{"url": "https://api.test.com"}],
        "paths": {
            "/users": {
                "get": {"operationId": "listUsers"},
                "post": {"operationId": "createUser"}
            },
            "/users/{id}": {
                "get": {"operationId": "getUser"},
                "delete": {"operationId": "deleteUser"}
            }
        }
    }


# ==============================================================================
# Test 1: Prance-Based Inline Schema Refs
# ==============================================================================

class TestInlineSchemaRefs:
    """Test that $ref references are properly resolved."""

    def test_prance_available(self):
        """Test that prance is available."""
        try:
            from prance import ResolvingParser
            assert True, "Prance is available"
        except ImportError:
            pytest.skip("Prance not installed - tests will use fallback parser")

    def test_simple_internal_refs_resolved(self, simple_spec_with_refs):
        """Test that simple internal $refs are resolved."""
        toolkit = OpenAPIToolkit(
            spec=simple_spec_with_refs,
            service="test",
            debug=True
        )

        # Spec should be resolved (no $refs in final spec)
        spec_str = json.dumps(toolkit.spec)

        # After resolution, there should be no $ref strings
        # (prance inlines all references)
        # Note: We can't guarantee 0 because some refs might remain
        # in responses, but the main schema should be resolved
        assert toolkit.spec is not None
        assert "paths" in toolkit.spec

    def test_nested_refs_resolved(self, simple_spec_with_refs):
        """Test that nested $refs (User -> Address) are resolved."""
        toolkit = OpenAPIToolkit(
            spec=simple_spec_with_refs,
            service="test"
        )

        # The spec should have been parsed successfully
        assert len(toolkit.operations) == 1
        assert toolkit.operations[0]['operation_id'] == 'listUsers'

    def test_fallback_without_prance(self, simple_spec_with_refs, monkeypatch):
        """Test that fallback works when prance is not available."""
        # Temporarily make prance unavailable
        import sys
        prance_module = sys.modules.get('prance')
        if prance_module:
            monkeypatch.setitem(sys.modules, 'prance', None)

        # Should still work with fallback parser
        toolkit = OpenAPIToolkit(
            spec=simple_spec_with_refs,
            service="test"
        )

        assert toolkit.spec is not None
        assert len(toolkit.operations) > 0


# ==============================================================================
# Test 2: Form-URLEncoded Support
# ==============================================================================

class TestFormUrlEncodedSupport:
    """Test support for application/x-www-form-urlencoded."""

    def test_form_encoded_detected(self, form_encoded_spec):
        """Test that form-urlencoded content type is detected."""
        toolkit = OpenAPIToolkit(
            spec=form_encoded_spec,
            service="auth"
        )

        # Check that operation was parsed correctly
        assert len(toolkit.operations) == 1
        operation = toolkit.operations[0]

        # Verify content type is detected
        assert operation['request_body'] is not None
        assert operation['request_body']['content_type'] == 'application/x-www-form-urlencoded'

    def test_form_encoded_schema_fields(self, form_encoded_spec):
        """Test that form fields are extracted to schema."""
        toolkit = OpenAPIToolkit(
            spec=form_encoded_spec,
            service="auth"
        )

        tools = toolkit.get_tools()
        assert len(tools) == 1

        tool = tools[0]
        schema = tool.get_tool_schema()

        # Schema should have username and password fields
        assert 'username' in schema['parameters']['properties']
        assert 'password' in schema['parameters']['properties']

        # Both should be required
        assert 'username' in schema['parameters']['required']
        assert 'password' in schema['parameters']['required']

    def test_json_prioritized_over_form(self, json_and_form_spec):
        """Test that JSON is prioritized when both are available."""
        toolkit = OpenAPIToolkit(
            spec=json_and_form_spec,
            service="api"
        )

        operation = toolkit.operations[0]

        # Should prefer JSON over form-urlencoded
        assert operation['request_body']['content_type'] == 'application/json'

    def test_form_only_when_no_json(self, form_encoded_spec):
        """Test that form-urlencoded is used when JSON is not available."""
        toolkit = OpenAPIToolkit(
            spec=form_encoded_spec,
            service="auth"
        )

        operation = toolkit.operations[0]
        assert operation['request_body']['content_type'] == 'application/x-www-form-urlencoded'

    @pytest.mark.asyncio
    async def test_form_request_execution(self, form_encoded_spec, mocker):
        """Test that form data is sent correctly in requests."""
        toolkit = OpenAPIToolkit(
            spec=form_encoded_spec,
            service="auth"
        )

        # Mock HTTPService.request
        mock_request = mocker.patch.object(
            toolkit.http_service,
            'request',
            return_value=({"success": True}, None)
        )

        # Execute the login operation
        tools = toolkit.get_tools()
        login_tool = tools[0]

        result = await login_tool._execute(
            username="testuser",
            password="testpass"
        )

        # Verify request was called with correct parameters
        assert mock_request.called
        call_kwargs = mock_request.call_args.kwargs

        # Should NOT use JSON
        assert call_kwargs.get('use_json') == False

        # Should have form data
        assert call_kwargs.get('data') == {
            'username': 'testuser',
            'password': 'testpass'
        }

        # Should have correct content-type header
        headers = call_kwargs.get('headers', {})
        assert headers.get('Content-Type') == 'application/x-www-form-urlencoded'


# ==============================================================================
# Test 3: Single-Operation Optimization
# ==============================================================================

class TestSingleOperationOptimization:
    """Test schema optimization for single-operation specs."""

    def test_single_operation_detected(self, single_operation_spec):
        """Test that single-operation mode is detected."""
        toolkit = OpenAPIToolkit(
            spec=single_operation_spec,
            service="users"
        )

        assert toolkit.is_single_operation is True
        assert len(toolkit.operations) == 1

    def test_multi_operation_detected(self, multi_operation_spec):
        """Test that multi-operation mode is detected."""
        toolkit = OpenAPIToolkit(
            spec=multi_operation_spec,
            service="users"
        )

        assert toolkit.is_single_operation is False
        assert len(toolkit.operations) == 4  # 4 operations total

    def test_single_operation_schema_simplified(self, single_operation_spec):
        """Test that single-operation schema is simplified (no path/method)."""
        toolkit = OpenAPIToolkit(
            spec=single_operation_spec,
            service="users"
        )

        tools = toolkit.get_tools()
        tool = tools[0]
        schema = tool.get_tool_schema()

        # Schema should have actual parameters
        properties = schema['parameters']['properties']
        assert 'userId' in properties
        assert 'include_details' in properties

        # Should NOT have meta fields (path, method) for single operation
        # Note: This is implicit - we just verify the params are there
        # and the schema is clean

        # Verify required field
        assert 'userId' in schema['parameters']['required']
        assert 'include_details' not in schema['parameters'].get('required', [])

    def test_multi_operation_schema_normal(self, multi_operation_spec):
        """Test that multi-operation schemas are normal (include all fields)."""
        toolkit = OpenAPIToolkit(
            spec=multi_operation_spec,
            service="users"
        )

        # All operations should be created
        assert len(toolkit.operations) == 4
        tools = toolkit.get_tools()
        assert len(tools) == 4

    def test_single_operation_fewer_tokens(self, single_operation_spec, multi_operation_spec):
        """Test that single-operation schemas use fewer tokens."""
        # Create single-operation toolkit
        single_toolkit = OpenAPIToolkit(
            spec=single_operation_spec,
            service="single"
        )

        # Create a modified multi-operation spec with just one operation
        modified_multi = {
            **multi_operation_spec,
            "paths": {
                "/users/{id}": multi_operation_spec["paths"]["/users/{id}"]
            }
        }
        # Remove one of the operations to make it single
        modified_multi["paths"]["/users/{id}"] = {
            "get": multi_operation_spec["paths"]["/users/{id}"]["get"]
        }

        multi_toolkit = OpenAPIToolkit(
            spec=modified_multi,
            service="multi"
        )

        # Get schemas
        single_schema = single_toolkit.get_tools()[0].get_tool_schema()

        # Single-operation should be cleaner/smaller
        # (This is a qualitative test - mainly checking it works)
        assert single_toolkit.is_single_operation is True


# ==============================================================================
# Integration Tests
# ==============================================================================

class TestIntegration:
    """Integration tests combining multiple features."""

    def test_all_improvements_together(self):
        """Test all three improvements working together."""
        spec = {
            "openapi": "3.0.0",
            "info": {"title": "Complete API", "version": "1.0.0"},
            "servers": [{"url": "https://api.test.com"}],
            "paths": {
                "/auth/login": {
                    "post": {
                        "operationId": "login",
                        "requestBody": {
                            "required": True,
                            "content": {
                                "application/x-www-form-urlencoded": {
                                    "schema": {
                                        "$ref": "#/components/schemas/LoginRequest"
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "components": {
                "schemas": {
                    "LoginRequest": {
                        "type": "object",
                        "required": ["username", "password"],
                        "properties": {
                            "username": {"type": "string"},
                            "password": {"type": "string"}
                        }
                    }
                }
            }
        }

        toolkit = OpenAPIToolkit(spec=spec, service="auth")

        # Test 1: Prance resolved $ref
        operation = toolkit.operations[0]
        assert operation['request_body'] is not None

        # Test 2: Form-urlencoded detected
        assert operation['request_body']['content_type'] == 'application/x-www-form-urlencoded'

        # Test 3: Single operation optimization
        assert toolkit.is_single_operation is True

        # Verify tool works
        tools = toolkit.get_tools()
        assert len(tools) == 1

        schema = tools[0].get_tool_schema()
        assert 'username' in schema['parameters']['properties']
        assert 'password' in schema['parameters']['properties']

    def test_real_world_petstore(self):
        """Test with a real-world OpenAPI spec (Petstore)."""
        # Simplified Petstore spec
        petstore_spec = {
            "openapi": "3.0.0",
            "info": {"title": "Petstore", "version": "1.0.0"},
            "servers": [{"url": "https://petstore.swagger.io/v2"}],
            "paths": {
                "/pet/{petId}": {
                    "get": {
                        "operationId": "getPetById",
                        "parameters": [
                            {
                                "name": "petId",
                                "in": "path",
                                "required": True,
                                "schema": {"type": "integer"}
                            }
                        ]
                    }
                },
                "/pet": {
                    "post": {
                        "operationId": "addPet",
                        "requestBody": {
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "$ref": "#/components/schemas/Pet"
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "components": {
                "schemas": {
                    "Pet": {
                        "type": "object",
                        "required": ["name"],
                        "properties": {
                            "id": {"type": "integer"},
                            "name": {"type": "string"},
                            "category": {"$ref": "#/components/schemas/Category"}
                        }
                    },
                    "Category": {
                        "type": "object",
                        "properties": {
                            "id": {"type": "integer"},
                            "name": {"type": "string"}
                        }
                    }
                }
            }
        }

        toolkit = OpenAPIToolkit(
            spec=petstore_spec,
            service="petstore"
        )

        # Should create 2 tools
        tools = toolkit.get_tools()
        assert len(tools) == 2

        # Not single operation
        assert toolkit.is_single_operation is False

        # Verify tool names
        tool_names = [t.name for t in tools]
        assert 'petstore_get_pet_petid' in tool_names
        assert 'petstore_post_pet' in tool_names


# ==============================================================================
# Error Handling Tests
# ==============================================================================

class TestErrorHandling:
    """Test error handling and edge cases."""

    def test_missing_base_url_raises_error(self):
        """Test that missing base URL raises clear error."""
        spec = {
            "openapi": "3.0.0",
            "info": {"title": "No Server API", "version": "1.0.0"},
            "paths": {
                "/test": {
                    "get": {"operationId": "test"}
                }
            }
        }

        with pytest.raises(ValueError, match="No base URL found"):
            OpenAPIToolkit(spec=spec, service="test")

    def test_relative_base_url_raises_error(self):
        """Test that relative base URL raises clear error."""
        spec = {
            "openapi": "3.0.0",
            "info": {"title": "Relative API", "version": "1.0.0"},
            "servers": [{"url": "/api/v1"}],
            "paths": {"/test": {"get": {"operationId": "test"}}}
        }

        with pytest.raises(ValueError, match="Base URL .* is relative"):
            OpenAPIToolkit(spec=spec, service="test")

    def test_missing_protocol_raises_error(self):
        """Test that missing protocol in base URL raises error."""
        spec = {
            "openapi": "3.0.0",
            "info": {"title": "No Protocol API", "version": "1.0.0"},
            "servers": [{"url": "api.example.com"}],
            "paths": {"/test": {"get": {"operationId": "test"}}}
        }

        with pytest.raises(ValueError, match="missing protocol"):
            OpenAPIToolkit(spec=spec, service="test")

    def test_empty_spec_handled_gracefully(self):
        """Test that empty spec is handled gracefully."""
        spec = {
            "openapi": "3.0.0",
            "info": {"title": "Empty API", "version": "1.0.0"},
            "servers": [{"url": "https://api.test.com"}],
            "paths": {}
        }

        toolkit = OpenAPIToolkit(spec=spec, service="test")

        # Should have no operations
        assert len(toolkit.operations) == 0
        assert len(toolkit.get_tools()) == 0


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