import json

from tests.fixtures.spans import get_otlp_wire_payload, get_span_json
from verse_sdk import OtelGenAISpecCheck


class TestSpecCheckMetadata:
    def test_metadata_structure(self):
        span = get_span_json("valid_chat_complete")
        checker = OtelGenAISpecCheck(span)
        metadata = checker.extract_metadata()
        assert metadata["environment"] == "dev"
        assert metadata["model_provider"] == "anthropic"
        assert metadata["model"] == "claude-3-5-sonnet-20241022"
        assert metadata["project_id"] == "proj_1234567890"
        assert metadata["session_id"] == "sess_1234567890"
        assert metadata["start_time"] is not None
        assert metadata["end_time"] is not None
        assert metadata["span_id"] is not None
        assert metadata["span_parent_id"] is not None
        assert metadata["trace_id"] is not None
        assert metadata["user_id"] == "user_1234567890"


class TestValidSpans:
    def test_valid_chat_complete_has_all_common_fields(self):
        span = get_span_json("valid_chat_complete")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is False
        assert checker.is_attribute_valid("gen_ai.operation.name") is True

    def test_embeddings_complete_has_dimension_count(self):
        span = get_span_json("embeddings_complete")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_deprecations is False
        assert checker.is_attribute_valid("gen_ai.embeddings.dimension.count") is True

    def test_tool_execution_has_tool_fields(self):
        span = get_span_json("tool_execution")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_deprecations is False
        assert checker.is_attribute_valid("gen_ai.tool.name") is True
        assert checker.is_attribute_valid("gen_ai.tool.type") is True
        assert checker.is_attribute_valid("gen_ai.tool.description") is True


class TestMissingRequiredFields:
    def test_missing_operation_and_provider(self):
        span = get_span_json("missing_required_fields")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.operation.name") is True
        assert checker.is_attribute_invalid("gen_ai.provider.name") is True


class TestMissingRecommendedFields:
    def test_missing_recommended_fields_shows_warnings(self):
        span = get_span_json("missing_recommended_fields")
        checker = OtelGenAISpecCheck(span)

        assert checker.could_improve is True
        assert checker.is_attribute_missing("gen_ai.response.model") is True
        assert checker.is_attribute_missing("gen_ai.usage.input_tokens") is True


class TestInvalidEnumValues:
    def test_invalid_operation_name(self):
        span = get_span_json("invalid_enum_values")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.operation.name") is True

    def test_invalid_provider_name(self):
        span = get_span_json("invalid_enum_values")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.provider.name") is True

    def test_invalid_finish_reason(self):
        span = get_span_json("invalid_enum_values")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.response.finish_reasons") is True

    def test_invalid_output_type(self):
        span = get_span_json("invalid_enum_values")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.output.type") is True


class TestDeprecatedAttributes:
    def test_deprecated(self):
        span = get_span_json("deprecated_attributes")
        checker = OtelGenAISpecCheck(span)
        assert checker.is_attribute_deprecated("gen_ai.system") is True
        assert checker.is_attribute_deprecated("gen_ai.prompt") is True
        assert checker.is_attribute_deprecated("gen_ai.completion") is True


class TestInvalidInput:
    def test_invalid_json_string(self):
        with open("tests/fixtures/spans/invalid_json.txt") as f:
            invalid_json = f.read()

        checker = OtelGenAISpecCheck(invalid_json)
        assert checker.has_errors is True

    def test_dict_input(self):
        span_dict = json.loads(get_span_json("valid_chat_complete"))
        checker = OtelGenAISpecCheck(span_dict)
        assert checker.has_errors is False


class TestInvalidTypes:
    def test_invalid_string_type(self):
        span = get_span_json("invalid_types")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.operation.name") is True

    def test_invalid_int_type(self):
        span = get_span_json("invalid_types")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.request.max_tokens") is True
        assert checker.is_attribute_invalid("gen_ai.usage.input_tokens") is True
        assert checker.is_attribute_invalid("server.port") is True

    def test_invalid_double_type(self):
        span = get_span_json("invalid_types")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.request.temperature") is True

    def test_invalid_string_array_type(self):
        span = get_span_json("invalid_types")
        checker = OtelGenAISpecCheck(span)

        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.response.finish_reasons") is True

    def test_valid_attributes_are_still_valid(self):
        span = get_span_json("invalid_types")
        checker = OtelGenAISpecCheck(span)

        # These attributes have correct types and should be valid
        assert checker.is_attribute_valid("gen_ai.provider.name") is True
        assert checker.is_attribute_valid("gen_ai.request.top_p") is True
        assert checker.is_attribute_valid("gen_ai.usage.output_tokens") is True


class TestValidationDetails:
    def test_validation_details_structure(self):
        span = get_span_json("missing_required_fields")
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.operation.name") is True
        assert checker.is_attribute_invalid("gen_ai.provider.name") is True

    def test_error_count_matches_errors(self):
        span = get_span_json("invalid_enum_values")
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.operation.name") is True
        assert checker.is_attribute_invalid("gen_ai.provider.name") is True


class TestFromOtlp:
    """Tests for the from_otlp classmethod that converts OTLP wire format."""

    def test_from_otlp_json_extracts_spans(self):
        """Test that from_otlp_json correctly converts wire format spans."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        checkers = list(OtelGenAISpecCheck.from_otlp_json(payload))
        assert len(checkers) == 2
        assert checkers[0].has_errors is True
        assert checkers[1].has_errors is False

    def test_from_otlp_extracts_spans(self):
        """Test that from_otlp correctly converts wire format spans."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        resource = resource_span["resource"]
        scope_span = resource_span["scopeSpans"][0]
        scope = scope_span["scope"]

        # Get the second span (has gen_ai attributes)
        wire_span = scope_span["spans"][1]

        checker = OtelGenAISpecCheck.from_otlp(wire_span, resource, scope)

        # Should not have errors for a valid span
        assert checker.has_errors is False

    def test_from_otlp_extracts_metadata(self):
        """Test that metadata is correctly extracted from wire format."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        resource = resource_span["resource"]
        scope_span = resource_span["scopeSpans"][0]
        wire_span = scope_span["spans"][1]  # The gen_ai span

        checker = OtelGenAISpecCheck.from_otlp(wire_span, resource)
        metadata = checker.extract_metadata()

        assert metadata["project_id"] == "27d0f825-2f93-4f93-818a-b66baa255815"
        assert metadata["session_id"] == "9e3348dc-98e4-4ac4-8655-1d238589cdf7"
        assert metadata["user_id"] == "90995714-53f5-4341-9220-8ef044482926"
        assert metadata["model"] == "gpt-4-turbo"
        assert metadata["model_provider"] == "openai"
        # trace_id/span_id are converted to int by format_hex_id
        assert metadata["trace_id"] == int("978a0a326a44054e1736d3a8ffab40e9", 16)
        assert metadata["span_id"] == int("ce663f392b16e16a", 16)
        assert metadata["span_parent_id"] == int("0db215aec9097552", 16)

    def test_from_otlp_converts_attributes(self):
        """Test that wire format attributes are correctly flattened."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        resource = resource_span["resource"]
        wire_span = resource_span["scopeSpans"][0]["spans"][1]

        checker = OtelGenAISpecCheck.from_otlp(wire_span, resource)

        # Check that gen_ai attributes are accessible
        assert checker.get_attribute_value("gen_ai.operation.name") == "chat"
        assert checker.get_attribute_value("gen_ai.provider.name") == "openai"
        assert checker.get_attribute_value("gen_ai.request.model") == "gpt-4-turbo"

    def test_from_otlp_converts_int_values(self):
        """Test that intValue attributes are correctly converted to int."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        wire_span = resource_span["scopeSpans"][0]["spans"][1]

        checker = OtelGenAISpecCheck.from_otlp(wire_span)

        # intValue in wire format is often a string, should be converted to int
        input_tokens = checker.get_attribute_value("gen_ai.usage.input_tokens")
        assert input_tokens == 1027
        assert isinstance(input_tokens, int)

    def test_from_otlp_converts_array_values(self):
        """Test that arrayValue attributes are correctly converted."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        wire_span = resource_span["scopeSpans"][0]["spans"][1]

        checker = OtelGenAISpecCheck.from_otlp(wire_span)

        finish_reasons = checker.get_attribute_value("gen_ai.response.finish_reasons")
        assert finish_reasons == ["stop"]
        assert isinstance(finish_reasons, list)

    def test_from_otlp_converts_timestamps(self):
        """Test that nanosecond timestamps are converted to ISO format."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        wire_span = resource_span["scopeSpans"][0]["spans"][1]

        checker = OtelGenAISpecCheck.from_otlp(wire_span)
        metadata = checker.extract_metadata()

        # Timestamps should be ISO format strings
        assert metadata["start_time"] is not None
        assert metadata["end_time"] is not None
        assert "T" in metadata["start_time"]  # ISO format check
        assert metadata["start_time"].endswith("Z")

    def test_from_otlp_merges_resource_attributes(self):
        """Test that resource-level attributes are merged into span attributes."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        resource = resource_span["resource"]
        wire_span = resource_span["scopeSpans"][0]["spans"][0]  # Root span

        checker = OtelGenAISpecCheck.from_otlp(wire_span, resource)

        # Resource attributes should be accessible
        assert checker.get_attribute_value("service.name") == "pokemon-assistant"
        assert checker.get_attribute_value("telemetry.sdk.name") == "opentelemetry"

    def test_from_otlp_span_attrs_override_resource_attrs(self):
        """Test that span-level attributes override resource-level ones."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        resource_span = payload["resourceSpans"][0]
        resource = resource_span["resource"]
        wire_span = resource_span["scopeSpans"][0]["spans"][1]

        checker = OtelGenAISpecCheck.from_otlp(wire_span, resource)

        # Both resource and span have these, span should win
        assert (
            checker.get_attribute_value("project.id")
            == "27d0f825-2f93-4f93-818a-b66baa255815"
        )
        assert (
            checker.get_attribute_value("session.id")
            == "9e3348dc-98e4-4ac4-8655-1d238589cdf7"
        )

    def test_from_otlp_without_resource(self):
        """Test that from_otlp works without resource (span-only)."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        wire_span = payload["resourceSpans"][0]["scopeSpans"][0]["spans"][1]

        # Should not raise, just won't have resource attributes
        checker = OtelGenAISpecCheck.from_otlp(wire_span)

        assert checker.get_attribute_value("gen_ai.operation.name") == "chat"

    def test_from_otlp_iteration_pattern(self):
        """Test the recommended iteration pattern for processing S3 payloads."""
        payload = get_otlp_wire_payload("otlp_wire_format")
        checkers = []

        for resource_span in payload.get("resourceSpans", []):
            resource = resource_span.get("resource")
            for scope_span in resource_span.get("scopeSpans", []):
                scope = scope_span.get("scope")
                for span in scope_span.get("spans", []):
                    checker = OtelGenAISpecCheck.from_otlp(span, resource, scope)
                    checkers.append(checker)

        # Should have processed 2 spans
        assert len(checkers) == 2

        # First span is root (no gen_ai attrs, will have errors)
        assert checkers[0].has_errors is True

        # Second span has gen_ai attributes
        assert checkers[1].has_errors is False
        assert checkers[1].get_attribute_value("gen_ai.operation.name") == "chat"


class TestMessageValidation:
    def test_valid_input_messages(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [
                        {
                            "role": "user",
                            "parts": [{"type": "text", "content": "Hello"}],
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is False
        assert checker.is_attribute_valid("gen_ai.input.messages.0.role") is True
        assert (
            checker.is_attribute_valid("gen_ai.input.messages.0.parts.0.type") is True
        )
        assert (
            checker.is_attribute_valid("gen_ai.input.messages.0.parts.0.content")
            is True
        )

    def test_valid_output_messages(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.output.messages": json.dumps(
                    [
                        {
                            "role": "assistant",
                            "parts": [{"type": "text", "content": "Hi there"}],
                            "finish_reason": "stop",
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is False
        assert checker.is_attribute_valid("gen_ai.output.messages.0.role") is True
        assert (
            checker.is_attribute_valid("gen_ai.output.messages.0.finish_reason") is True
        )
        assert (
            checker.is_attribute_valid("gen_ai.output.messages.0.parts.0.type") is True
        )

    def test_missing_message_role(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [{"parts": [{"type": "text", "content": "Hello"}]}]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.input.messages.0.role") is True

    def test_missing_message_parts(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps([{"role": "user"}]),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.input.messages.0.parts") is True

    def test_missing_output_finish_reason(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.output.messages": json.dumps(
                    [
                        {
                            "role": "assistant",
                            "parts": [{"type": "text", "content": "Hi"}],
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert (
            checker.is_attribute_invalid("gen_ai.output.messages.0.finish_reason")
            is True
        )

    def test_invalid_message_role(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [
                        {
                            "role": "invalid_role",
                            "parts": [{"type": "text", "content": "Hello"}],
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert checker.is_attribute_invalid("gen_ai.input.messages.0.role") is True

    def test_missing_part_type(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [{"role": "user", "parts": [{"content": "Hello"}]}]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert (
            checker.is_attribute_invalid("gen_ai.input.messages.0.parts.0.type") is True
        )

    def test_missing_text_part_content(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [{"role": "user", "parts": [{"type": "text"}]}]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert (
            checker.is_attribute_invalid("gen_ai.input.messages.0.parts.0.content")
            is True
        )

    def test_missing_tool_call_part_name(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [
                        {
                            "role": "assistant",
                            "parts": [{"type": "tool_call", "id": "call_123"}],
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert (
            checker.is_attribute_invalid("gen_ai.input.messages.0.parts.0.name") is True
        )

    def test_missing_tool_call_response_response(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [
                        {
                            "role": "tool",
                            "parts": [{"type": "tool_call_response", "id": "call_123"}],
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is True
        assert (
            checker.is_attribute_invalid("gen_ai.input.messages.0.parts.0.response")
            is True
        )

    def test_multiple_messages_validation(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [
                        {
                            "role": "user",
                            "parts": [{"type": "text", "content": "Hello"}],
                        },
                        {
                            "role": "assistant",
                            "parts": [{"type": "text", "content": "Hi"}],
                        },
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is False
        assert checker.is_attribute_valid("gen_ai.input.messages.0.role") is True
        assert checker.is_attribute_valid("gen_ai.input.messages.1.role") is True

    def test_multiple_parts_validation(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps(
                    [
                        {
                            "role": "user",
                            "parts": [
                                {"type": "text", "content": "Hello"},
                                {
                                    "type": "tool_call",
                                    "name": "search",
                                    "id": "call_123",
                                },
                            ],
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is False
        assert (
            checker.is_attribute_valid("gen_ai.input.messages.0.parts.0.type") is True
        )
        assert (
            checker.is_attribute_valid("gen_ai.input.messages.0.parts.1.type") is True
        )
        assert (
            checker.is_attribute_valid("gen_ai.input.messages.0.parts.1.name") is True
        )

    def test_output_message_blob_part(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.output.messages": json.dumps(
                    [
                        {
                            "role": "assistant",
                            "parts": [
                                {
                                    "type": "blob",
                                    "content": "base64data",
                                    "modality": "image",
                                }
                            ],
                            "finish_reason": "stop",
                        }
                    ]
                ),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is False
        assert (
            checker.is_attribute_valid("gen_ai.output.messages.0.parts.0.type") is True
        )
        assert (
            checker.is_attribute_valid("gen_ai.output.messages.0.parts.0.content")
            is True
        )
        assert (
            checker.is_attribute_valid("gen_ai.output.messages.0.parts.0.modality")
            is True
        )

    def test_invalid_json_messages_attribute(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": "not valid json",
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is False

    def test_messages_not_a_list(self):
        span = {
            "context": {"trace_id": "123", "span_id": "456"},
            "attributes": {
                "gen_ai.operation.name": "chat",
                "gen_ai.provider.name": "anthropic",
                "gen_ai.input.messages": json.dumps({"not": "a list"}),
            },
        }
        checker = OtelGenAISpecCheck(span)
        assert checker.has_errors is False
