"""
Tests for core types.

Tests cover:
- ID generation and validation
- Span creation and manipulation
- Span serialization/deserialization
- Enum values
"""

import pytest
from datetime import datetime, timezone

from risicare_core import (
    # ID functions
    generate_trace_id,
    generate_span_id,
    generate_session_id,
    generate_agent_id,
    validate_trace_id,
    validate_span_id,
    utc_now,
    # Types
    Span,
    SpanEvent,
    SpanLink,
    ExceptionInfo,
    LLMAttributes,
    SpanKind,
    SpanStatus,
    SemanticPhase,
    AgentRole,
    MessageType,
)


# =============================================================================
# ID Generation Tests
# =============================================================================

class TestIDGeneration:
    def test_generate_trace_id_format(self):
        """generate_trace_id produces 32 hex characters."""
        trace_id = generate_trace_id()
        assert len(trace_id) == 32
        assert all(c in "0123456789abcdef" for c in trace_id)

    def test_generate_trace_id_unique(self):
        """generate_trace_id produces unique values."""
        ids = [generate_trace_id() for _ in range(100)]
        assert len(set(ids)) == 100

    def test_generate_span_id_format(self):
        """generate_span_id produces 16 hex characters."""
        span_id = generate_span_id()
        assert len(span_id) == 16
        assert all(c in "0123456789abcdef" for c in span_id)

    def test_generate_span_id_unique(self):
        """generate_span_id produces unique values."""
        ids = [generate_span_id() for _ in range(100)]
        assert len(set(ids)) == 100

    def test_generate_session_id_format(self):
        """generate_session_id produces valid UUID format."""
        session_id = generate_session_id()
        # UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        assert len(session_id) == 36
        assert session_id.count("-") == 4

    def test_generate_agent_id_format(self):
        """generate_agent_id produces valid format."""
        agent_id = generate_agent_id()
        # Format: agent_xxxxxxxx
        assert agent_id.startswith("agent_")


# =============================================================================
# ID Validation Tests
# =============================================================================

class TestIDValidation:
    def test_validate_trace_id_valid(self):
        """Valid trace_id passes validation."""
        assert validate_trace_id("a" * 32) is True
        assert validate_trace_id("0123456789abcdef" * 2) is True

    def test_validate_trace_id_invalid(self):
        """Invalid trace_id fails validation."""
        assert validate_trace_id("a" * 31) is False  # Too short
        assert validate_trace_id("a" * 33) is False  # Too long
        assert validate_trace_id("g" * 32) is False  # Invalid char (not hex)
        assert validate_trace_id("") is False        # Empty
        # W3C Trace Context requires lowercase hex
        assert validate_trace_id("A" * 32) is False  # Uppercase is invalid

    def test_validate_span_id_valid(self):
        """Valid span_id passes validation."""
        assert validate_span_id("b" * 16) is True
        assert validate_span_id("0123456789abcdef") is True

    def test_validate_span_id_invalid(self):
        """Invalid span_id fails validation."""
        assert validate_span_id("b" * 15) is False  # Too short
        assert validate_span_id("b" * 17) is False  # Too long
        assert validate_span_id("xyz" * 6) is False  # Invalid chars


# =============================================================================
# Timestamp Tests
# =============================================================================

class TestTimestamp:
    def test_utc_now_returns_utc(self):
        """utc_now returns timezone-aware UTC datetime."""
        now = utc_now()
        assert now.tzinfo is not None
        assert now.tzinfo == timezone.utc


# =============================================================================
# Span Tests
# =============================================================================

class TestSpan:
    def test_span_creation_minimal(self):
        """Span can be created with minimal arguments."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test",
        )

        assert span.trace_id == "a" * 32
        assert span.span_id == "b" * 16
        assert span.name == "test"
        assert span.parent_span_id is None
        assert span.kind == SpanKind.INTERNAL
        assert span.status == SpanStatus.UNSET

    def test_span_creation_full(self):
        """Span can be created with all arguments."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="full_span",
            parent_span_id="c" * 16,
            kind=SpanKind.LLM_CALL,
            status=SpanStatus.OK,
            attributes={"key": "value"},
        )

        assert span.parent_span_id == "c" * 16
        assert span.kind == SpanKind.LLM_CALL
        assert span.status == SpanStatus.OK
        assert span.attributes["key"] == "value"

    def test_span_validates_trace_id(self):
        """Span validates trace_id format."""
        with pytest.raises(ValueError, match="Invalid trace_id"):
            Span(
                trace_id="invalid",
                span_id="b" * 16,
                name="test",
            )

    def test_span_validates_span_id(self):
        """Span validates span_id format."""
        with pytest.raises(ValueError, match="Invalid span_id"):
            Span(
                trace_id="a" * 32,
                span_id="invalid",
                name="test",
            )

    def test_span_set_attribute(self):
        """Span.set_attribute adds attributes."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test",
        )

        span.set_attribute("key1", "value1")
        span.set_attribute("key2", 42)

        assert span.attributes["key1"] == "value1"
        assert span.attributes["key2"] == 42

    def test_span_add_event(self):
        """Span.add_event adds events."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test",
        )

        span.add_event("event1", {"data": "foo"})
        span.add_event("event2")

        assert len(span.events) == 2
        assert span.events[0].name == "event1"
        assert span.events[0].attributes["data"] == "foo"
        assert span.events[1].name == "event2"

    def test_span_record_exception(self):
        """Span.record_exception records exception info."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test",
        )

        try:
            raise ValueError("test error")
        except ValueError as e:
            span.record_exception(e)

        assert len(span.exceptions) == 1
        assert span.exceptions[0].type == "ValueError"
        assert span.exceptions[0].message == "test error"
        assert span.status == SpanStatus.ERROR

    def test_span_end(self):
        """Span.end sets end_time."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test",
        )

        assert span.end_time is None

        span.end()

        assert span.end_time is not None
        assert span.end_time >= span.start_time

    def test_span_duration(self):
        """Span.duration_ms calculates duration."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test",
        )

        # Not ended yet
        assert span.duration_ms is None

        span.end()

        duration = span.duration_ms
        assert duration is not None
        assert duration >= 0


# =============================================================================
# Span Serialization Tests
# =============================================================================

class TestSpanSerialization:
    def test_span_to_dict(self):
        """Span.to_dict produces correct structure (camelCase per gateway API)."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test_span",
            kind=SpanKind.LLM_CALL,
        )
        span.set_attribute("model", "gpt-4")
        span.end()

        d = span.to_dict()

        # Uses camelCase per gateway API contract
        assert d["traceId"] == "a" * 32
        assert d["spanId"] == "b" * 16
        assert d["name"] == "test_span"
        assert d["kind"] == "llm_call"
        assert d["attributes"]["model"] == "gpt-4"
        assert "startTime" in d
        assert "endTime" in d

    def test_span_to_dict_always_includes_collections(self):
        """Span.to_dict always includes events, links, exceptions."""
        span = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test",
        )

        d = span.to_dict()

        assert "attributes" in d
        assert "events" in d
        assert "links" in d
        assert "exceptions" in d
        assert isinstance(d["attributes"], dict)
        assert isinstance(d["events"], list)

    def test_span_from_dict(self):
        """Span.from_dict reconstructs span."""
        original = Span(
            trace_id="a" * 32,
            span_id="b" * 16,
            name="test_span",
            kind=SpanKind.AGENT,
        )
        original.set_attribute("key", "value")
        original.add_event("my_event", {"data": 123})
        original.end()

        d = original.to_dict()
        reconstructed = Span.from_dict(d)

        assert reconstructed.trace_id == original.trace_id
        assert reconstructed.span_id == original.span_id
        assert reconstructed.name == original.name
        assert reconstructed.kind == original.kind
        assert reconstructed.attributes["key"] == "value"
        assert len(reconstructed.events) == 1
        assert reconstructed.events[0].name == "my_event"

    def test_span_event_serialization(self):
        """SpanEvent serializes and deserializes."""
        event = SpanEvent(
            name="test_event",
            attributes={"key": "value"},
        )

        d = event.to_dict()
        reconstructed = SpanEvent.from_dict(d)

        assert reconstructed.name == event.name
        assert reconstructed.attributes == event.attributes

    def test_exception_info_serialization(self):
        """ExceptionInfo serializes and deserializes."""
        exc_info = ExceptionInfo(
            type="ValueError",
            message="test error",
            stacktrace="...",
        )

        d = exc_info.to_dict()
        reconstructed = ExceptionInfo.from_dict(d)

        assert reconstructed.type == exc_info.type
        assert reconstructed.message == exc_info.message


# =============================================================================
# LLM Attributes Tests
# =============================================================================

class TestLLMAttributes:
    def test_llm_attributes_creation(self):
        """LLMAttributes can be created."""
        attrs = LLMAttributes(
            provider="openai",
            model="gpt-4",
            prompt_tokens=100,
            completion_tokens=50,
            total_tokens=150,
        )

        assert attrs.provider == "openai"
        assert attrs.model == "gpt-4"
        assert attrs.total_tokens == 150

    def test_llm_attributes_serialization(self):
        """LLMAttributes serializes and deserializes."""
        attrs = LLMAttributes(
            provider="anthropic",
            model="claude-3",
            prompt_tokens=200,
            completion_tokens=100,
            total_tokens=300,
            temperature=0.7,
        )

        d = attrs.to_dict()
        reconstructed = LLMAttributes.from_dict(d)

        assert reconstructed.provider == attrs.provider
        assert reconstructed.model == attrs.model
        assert reconstructed.temperature == attrs.temperature


# =============================================================================
# Enum Tests
# =============================================================================

class TestEnums:
    def test_span_kind_values(self):
        """SpanKind has expected values."""
        assert SpanKind.INTERNAL.value == "internal"
        assert SpanKind.LLM_CALL.value == "llm_call"
        assert SpanKind.AGENT.value == "agent"
        assert SpanKind.DELEGATION.value == "delegation"

    def test_span_status_values(self):
        """SpanStatus has expected values."""
        assert SpanStatus.UNSET.value == "unset"
        assert SpanStatus.OK.value == "ok"
        assert SpanStatus.ERROR.value == "error"

    def test_semantic_phase_values(self):
        """SemanticPhase has expected values."""
        assert SemanticPhase.THINK.value == "think"
        assert SemanticPhase.DECIDE.value == "decide"
        assert SemanticPhase.ACT.value == "act"
        assert SemanticPhase.OBSERVE.value == "observe"
        assert SemanticPhase.COORDINATE.value == "coordinate"

    def test_agent_role_values(self):
        """AgentRole has expected values."""
        assert AgentRole.ORCHESTRATOR.value == "orchestrator"
        assert AgentRole.WORKER.value == "worker"

    def test_message_type_values(self):
        """MessageType has expected values."""
        assert MessageType.TASK.value == "task"
        assert MessageType.RESULT.value == "result"
        assert MessageType.RESPONSE.value == "response"
