"""Tests for the Pydantic event schema models."""

import time

import pytest
from pydantic import ValidationError

from pendo_sdk.schema import (
    AgentEvent,
    ConversationOutcome,
    EventBatch,
    EventProps,
    EventType,
    ReactionType,
    SpanStatusCode,
    SpanType,
    ToolStatus,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _valid_event(**overrides) -> dict:
    """Return a minimal valid AgentEvent dict, with optional overrides."""
    defaults = {
        "visitor_id": "user@example.com",
        "account_id": "acct_123",
        "trace_id": "a" * 32,
        "span_id": "b" * 16,
        "type": SpanType.GENERATION,
        "name": "classify_intent",
        "start_time": 1700000000000,
        "end_time": 1700000001234,
        "duration_ms": 1234.0,
    }
    defaults.update(overrides)
    return defaults


# ---------------------------------------------------------------------------
# SpanType enum — full set from web SDK
# ---------------------------------------------------------------------------

class TestSpanType:
    def test_all_span_types(self):
        expected = {
            "AGENT", "GENERATION", "TOOL", "CHAIN", "RETRIEVAL",
            "GUARDRAIL", "USER_MESSAGE", "SIGNAL", "REACTION", "UNKNOWN",
        }
        actual = {t.value for t in SpanType}
        assert actual == expected

    def test_generation(self):
        assert SpanType.GENERATION == "GENERATION"

    def test_tool(self):
        assert SpanType.TOOL == "TOOL"

    def test_chain(self):
        assert SpanType.CHAIN == "CHAIN"

    def test_agent(self):
        assert SpanType.AGENT == "AGENT"

    def test_retrieval(self):
        assert SpanType.RETRIEVAL == "RETRIEVAL"

    def test_guardrail(self):
        assert SpanType.GUARDRAIL == "GUARDRAIL"

    def test_user_message(self):
        assert SpanType.USER_MESSAGE == "USER_MESSAGE"

    def test_signal(self):
        assert SpanType.SIGNAL == "SIGNAL"

    def test_reaction(self):
        assert SpanType.REACTION == "REACTION"

    def test_unknown(self):
        assert SpanType.UNKNOWN == "UNKNOWN"


# ---------------------------------------------------------------------------
# EventType enum
# ---------------------------------------------------------------------------

class TestEventType:
    def test_all_event_types(self):
        expected = {
            "message", "tool_call", "signal", "reaction",
            "span_start", "span_end", "conversation_start", "conversation_end",
        }
        actual = {t.value for t in EventType}
        assert actual == expected


# ---------------------------------------------------------------------------
# Supporting enums
# ---------------------------------------------------------------------------

class TestToolStatus:
    def test_values(self):
        assert ToolStatus.SUCCESS == "success"
        assert ToolStatus.ERROR == "error"


class TestReactionType:
    def test_all_reaction_types(self):
        expected = {"thumbs_up", "thumbs_down", "copy", "retry", "edit"}
        actual = {r.value for r in ReactionType}
        assert actual == expected


class TestConversationOutcome:
    def test_all_outcomes(self):
        expected = {"completed", "abandoned", "escalated", "error"}
        actual = {o.value for o in ConversationOutcome}
        assert actual == expected


# ---------------------------------------------------------------------------
# EventProps
# ---------------------------------------------------------------------------

class TestEventProps:
    def test_llm_props(self):
        props = EventProps(
            model=["claude-sonnet-4-20250514"],
            prompt_tokens=100,
            completion_tokens=50,
            total_tokens=150,
            cost_usd=0.003,
        )
        assert props.model == ["claude-sonnet-4-20250514"]
        assert props.prompt_tokens == 100
        assert props.completion_tokens == 50
        assert props.total_tokens == 150
        assert props.cost_usd == 0.003
        assert props.tool_name is None

    def test_tool_props(self):
        props = EventProps(
            tool_name="search_db",
            tool_input="{'query': 'hello'}",
            tool_output="[{'id': 1}]",
            tool_status=ToolStatus.SUCCESS,
        )
        assert props.tool_name == "search_db"
        assert props.tool_status == ToolStatus.SUCCESS
        assert props.model is None

    def test_message_props(self):
        props = EventProps(role="user", content="Hello, world!")
        assert props.role == "user"
        assert props.content == "Hello, world!"

    def test_signal_props(self):
        props = EventProps(
            signal_name="user_frustration",
            signal_data={"short_messages": 3, "window_ms": 5000},
        )
        assert props.signal_name == "user_frustration"
        assert props.signal_data["short_messages"] == 3

    def test_reaction_props(self):
        props = EventProps(
            reaction_type=ReactionType.THUMBS_UP,
            message_span_id="c" * 16,
        )
        assert props.reaction_type == ReactionType.THUMBS_UP
        assert props.message_span_id == "c" * 16

    def test_extra_attributes(self):
        props = EventProps(extra={"custom.key": "value"})
        assert props.extra["custom.key"] == "value"

    def test_empty_props(self):
        props = EventProps()
        assert props.model is None
        assert props.tool_name is None
        assert props.role is None
        assert props.signal_name is None
        assert props.reaction_type is None
        assert props.extra == {}

    def test_negative_tokens_rejected(self):
        with pytest.raises(ValidationError):
            EventProps(prompt_tokens=-1)

    def test_negative_cost_rejected(self):
        with pytest.raises(ValidationError):
            EventProps(cost_usd=-0.01)


# ---------------------------------------------------------------------------
# AgentEvent
# ---------------------------------------------------------------------------

class TestAgentEvent:
    def test_minimal_valid_event(self):
        event = AgentEvent(**_valid_event())
        assert event.visitor_id == "user@example.com"
        assert event.type == SpanType.GENERATION
        assert event.name == "classify_intent"
        assert event.sdk_name == "pendo_sdk"
        assert event.sdk_version == "0.1.0"

    def test_browser_time_defaults_to_now(self):
        before = int(time.time() * 1000)
        event = AgentEvent(**_valid_event())
        after = int(time.time() * 1000)
        assert before <= event.browser_time <= after

    def test_explicit_browser_time(self):
        event = AgentEvent(**_valid_event(browser_time=1700000000000))
        assert event.browser_time == 1700000000000

    def test_status_defaults_to_unset(self):
        event = AgentEvent(**_valid_event())
        assert event.status == SpanStatusCode.UNSET

    def test_error_status_with_message(self):
        event = AgentEvent(
            **_valid_event(status=SpanStatusCode.ERROR, error_message="timeout")
        )
        assert event.status == SpanStatusCode.ERROR
        assert event.error_message == "timeout"

    def test_parent_span_id_optional(self):
        event = AgentEvent(**_valid_event(parent_span_id=None))
        assert event.parent_span_id is None

    def test_parent_span_id_valid(self):
        event = AgentEvent(**_valid_event(parent_span_id="c" * 16))
        assert event.parent_span_id == "c" * 16

    def test_depth_defaults_to_zero(self):
        event = AgentEvent(**_valid_event())
        assert event.depth == 0

    def test_depth_explicit(self):
        event = AgentEvent(**_valid_event(depth=3))
        assert event.depth == 3

    def test_negative_depth_rejected(self):
        with pytest.raises(ValidationError):
            AgentEvent(**_valid_event(depth=-1))

    def test_conversation_id(self):
        event = AgentEvent(**_valid_event(conversation_id="conv_abc123"))
        assert event.conversation_id == "conv_abc123"

    def test_event_type(self):
        event = AgentEvent(**_valid_event(event_type=EventType.MESSAGE))
        assert event.event_type == EventType.MESSAGE

    def test_with_llm_props(self):
        event = AgentEvent(
            **_valid_event(
                type=SpanType.GENERATION,
                props=EventProps(
                    model=["gpt-4"],
                    prompt_tokens=200,
                    completion_tokens=80,
                    total_tokens=280,
                    cost_usd=0.01,
                ),
            )
        )
        assert event.props.model == ["gpt-4"]
        assert event.props.prompt_tokens == 200
        assert event.props.total_tokens == 280
        assert event.props.cost_usd == 0.01

    def test_with_tool_props(self):
        event = AgentEvent(
            **_valid_event(
                type=SpanType.TOOL,
                name="search_db",
                props=EventProps(
                    tool_name="search_db",
                    tool_input="{'q': 'test'}",
                    tool_output="[]",
                    tool_status=ToolStatus.SUCCESS,
                ),
            )
        )
        assert event.props.tool_name == "search_db"
        assert event.props.tool_status == ToolStatus.SUCCESS

    def test_with_signal_props(self):
        event = AgentEvent(
            **_valid_event(
                type=SpanType.SIGNAL,
                name="user_frustration",
                event_type=EventType.SIGNAL,
                props=EventProps(
                    signal_name="user_frustration",
                    signal_data={"count": 3},
                ),
            )
        )
        assert event.props.signal_name == "user_frustration"

    def test_with_reaction_props(self):
        event = AgentEvent(
            **_valid_event(
                type=SpanType.REACTION,
                name="thumbs_up",
                event_type=EventType.REACTION,
                props=EventProps(
                    reaction_type=ReactionType.THUMBS_UP,
                    message_span_id="d" * 16,
                ),
            )
        )
        assert event.props.reaction_type == ReactionType.THUMBS_UP

    def test_account_id_optional(self):
        event = AgentEvent(**_valid_event(account_id=None))
        assert event.account_id is None

    # --- Validation ---

    def test_visitor_id_required(self):
        data = _valid_event()
        del data["visitor_id"]
        with pytest.raises(ValidationError):
            AgentEvent(**data)

    def test_trace_id_must_be_32_hex(self):
        with pytest.raises(ValidationError):
            AgentEvent(**_valid_event(trace_id="short"))

    def test_trace_id_rejects_uppercase(self):
        with pytest.raises(ValidationError):
            AgentEvent(**_valid_event(trace_id="A" * 32))

    def test_span_id_must_be_16_hex(self):
        with pytest.raises(ValidationError):
            AgentEvent(**_valid_event(span_id="short"))

    def test_parent_span_id_validated(self):
        with pytest.raises(ValidationError):
            AgentEvent(**_valid_event(parent_span_id="bad"))

    def test_negative_duration_rejected(self):
        with pytest.raises(ValidationError):
            AgentEvent(**_valid_event(duration_ms=-1.0))


# ---------------------------------------------------------------------------
# to_wire() serialization
# ---------------------------------------------------------------------------

class TestToWire:
    def test_excludes_none_values(self):
        event = AgentEvent(**_valid_event())
        wire = event.to_wire()
        assert "parent_span_id" not in wire
        assert "error_message" not in wire
        assert "conversation_id" not in wire
        assert "event_type" not in wire
        # Props should also exclude None
        assert "model" not in wire["props"]
        assert "tool_name" not in wire["props"]
        assert "signal_name" not in wire["props"]
        assert "reaction_type" not in wire["props"]

    def test_includes_populated_props(self):
        event = AgentEvent(
            **_valid_event(
                props=EventProps(
                    model=["claude-sonnet-4-20250514"],
                    prompt_tokens=100,
                    completion_tokens=50,
                    total_tokens=150,
                ),
            )
        )
        wire = event.to_wire()
        assert wire["props"]["modelUsed"] == "claude-sonnet-4-20250514"
        assert wire["props"]["modelsUsed"] == ["claude-sonnet-4-20250514"]
        assert wire["props"]["promptTokens"] == 100
        assert wire["props"]["completionTokens"] == 50
        assert wire["props"]["totalTokens"] == 150

    def test_extra_flattened_into_props(self):
        event = AgentEvent(
            **_valid_event(
                props=EventProps(model=["gpt-4"], extra={"custom.tag": "hello"}),
            )
        )
        wire = event.to_wire()
        assert wire["props"]["custom.tag"] == "hello"
        assert "extra" not in wire["props"]

    def test_type_serialized_as_string(self):
        event = AgentEvent(**_valid_event(type=SpanType.TOOL))
        wire = event.to_wire()
        assert wire["type"] == "TOOL"

    def test_depth_included_in_wire(self):
        event = AgentEvent(**_valid_event(depth=2))
        wire = event.to_wire()
        assert wire["depth"] == 2

    def test_conversation_id_in_wire(self):
        event = AgentEvent(**_valid_event(conversation_id="conv_123"))
        wire = event.to_wire()
        assert wire["conversation_id"] == "conv_123"

    def test_tool_status_serialized(self):
        event = AgentEvent(
            **_valid_event(
                type=SpanType.TOOL,
                props=EventProps(tool_name="fn", tool_status=ToolStatus.ERROR),
            )
        )
        wire = event.to_wire()
        assert wire["props"]["toolStatus"] == "error"


# ---------------------------------------------------------------------------
# EventBatch
# ---------------------------------------------------------------------------

class TestEventBatch:
    def test_valid_batch(self):
        events = [AgentEvent(**_valid_event(name=f"span_{i}")) for i in range(3)]
        batch = EventBatch(events=events)
        assert len(batch.events) == 3

    def test_empty_batch_rejected(self):
        with pytest.raises(ValidationError):
            EventBatch(events=[])

    def test_batch_to_wire(self):
        events = [AgentEvent(**_valid_event())]
        batch = EventBatch(events=events)
        wire = batch.to_wire()
        assert "events" in wire
        assert len(wire["events"]) == 1
        assert wire["events"][0]["visitor_id"] == "user@example.com"


# ---------------------------------------------------------------------------
# Web-only fields are NOT present
# ---------------------------------------------------------------------------

class TestNoWebFields:
    """Confirm web-specific fields from the JZB format are not in our schema."""

    def test_no_tab_id(self):
        event = AgentEvent(**_valid_event())
        assert not hasattr(event, "tab_id")
        assert not hasattr(event, "tabId")

    def test_no_frame_id(self):
        event = AgentEvent(**_valid_event())
        assert not hasattr(event, "frame_id")

    def test_no_session_id(self):
        event = AgentEvent(**_valid_event())
        assert not hasattr(event, "session_id")

    def test_no_recording_fields(self):
        event = AgentEvent(**_valid_event())
        assert not hasattr(event, "recording_id")
        assert not hasattr(event, "recording_session_id")
