"""Tests for PendoSpanExporter, attribute mapping, and PII redaction."""

import pytest
from unittest.mock import patch, MagicMock

from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace.export import SpanExportResult
from opentelemetry.trace import StatusCode

from pendo_sdk.exporter import (
    PendoSpanExporter,
    _redact,
    _redact_attributes,
    _ns_to_ms,
)
from pendo_sdk.schema import SpanStatusCode, SpanType


# ---------------------------------------------------------------------------
# PII Redaction
# ---------------------------------------------------------------------------

class TestPIIRedaction:
    def test_redact_email(self):
        assert _redact("contact john@example.com please") == \
            "contact [EMAIL_REDACTED] please"

    def test_redact_phone(self):
        assert _redact("call 555-123-4567") == "call [PHONE_REDACTED]"
        assert _redact("call 555.123.4567") == "call [PHONE_REDACTED]"
        assert _redact("call 5551234567") == "call [PHONE_REDACTED]"

    def test_redact_ssn(self):
        assert _redact("ssn: 123-45-6789") == "ssn: [SSN_REDACTED]"

    def test_redact_no_pii(self):
        text = "The weather in Denver is sunny"
        assert _redact(text) == text

    def test_redact_multiple_patterns(self):
        text = "Email john@test.com, phone 555-123-4567, ssn 123-45-6789"
        result = _redact(text)
        assert "[EMAIL_REDACTED]" in result
        assert "[PHONE_REDACTED]" in result
        assert "[SSN_REDACTED]" in result

    def test_redact_attributes_only_strings(self):
        attrs = {
            "message": "contact john@test.com",
            "token_count": 42,
            "is_cached": True,
        }
        result = _redact_attributes(attrs)
        assert result["message"] == "contact [EMAIL_REDACTED]"
        assert result["token_count"] == 42
        assert result["is_cached"] is True


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

def _make_span(
    name="test-span",
    attrs=None,
    parent=None,
    start_ns=1_000_000_000,
    end_ns=1_500_000_000,
    status_code=StatusCode.OK,
    status_description=None,
):
    """Create a mock ReadableSpan for testing."""
    span = MagicMock(spec=ReadableSpan)
    span.name = name
    span.attributes = attrs or {}
    span.start_time = start_ns
    span.end_time = end_ns

    context = MagicMock()
    context.trace_id = 0x1234567890ABCDEF1234567890ABCDEF
    context.span_id = 0x1234567890ABCDEF
    span.context = context

    if parent:
        parent_ctx = MagicMock()
        parent_ctx.span_id = parent
        span.parent = parent_ctx
    else:
        span.parent = None

    status = MagicMock()
    status.status_code = status_code
    status.description = status_description
    span.status = status

    return span


def _make_exporter(**kwargs):
    defaults = {
        "api_key": "test-key",
        "endpoint": "https://test.pendo.io",
        "visitor_id": "user@example.com",
        "account_id": "acct_123",
    }
    defaults.update(kwargs)
    return PendoSpanExporter(**defaults)


# ---------------------------------------------------------------------------
# Attribute Mapping — _build_event
# ---------------------------------------------------------------------------

class TestBuildEvent:
    def test_basic_generation_span(self):
        exporter = _make_exporter()
        span = _make_span(
            name="messages.create",
            attrs={
                "pendo.span.type": "GENERATION",
                "llm.model_name": "claude-sonnet-4-20250514",
                "llm.token_count.prompt": 100,
                "llm.token_count.completion": 50,
            },
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.name == "messages.create"
        assert event.type == SpanType.GENERATION
        assert event.props.model == ["claude-sonnet-4-20250514"]
        assert event.props.prompt_tokens == 100
        assert event.props.completion_tokens == 50

    def test_tool_span_mapping(self):
        exporter = _make_exporter()
        span = _make_span(
            name="search_db",
            attrs={
                "pendo.span.type": "TOOL",
                "tool.name": "search_db",
                "tool.input": "{'q': 'hello'}",
                "tool.output": "[{'id': 1}]",
                "tool.status": "success",
            },
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.type == SpanType.TOOL
        assert event.props.tool_name == "search_db"
        assert event.props.tool_input == "{'q': 'hello'}"
        assert event.props.tool_output == "[{'id': 1}]"
        assert event.props.tool_status == "success"

    def test_identity_injected(self):
        exporter = _make_exporter(
            visitor_id="mick@pendo.io",
            account_id="acct_pendo",
        )
        span = _make_span(attrs={"pendo.span.type": "GENERATION"})
        event = exporter._build_event(span, dict(span.attributes))

        assert event.visitor_id == "mick@pendo.io"
        assert event.account_id == "acct_pendo"

    def test_anonymous_when_no_visitor(self):
        exporter = _make_exporter(visitor_id=None)
        span = _make_span(attrs={"pendo.span.type": "GENERATION"})
        event = exporter._build_event(span, dict(span.attributes))

        assert event.visitor_id == "anonymous"

    def test_timestamps_converted_to_ms(self):
        exporter = _make_exporter()
        span = _make_span(
            start_ns=1_700_000_000_000_000_000,  # 1.7 trillion ns
            end_ns=1_700_000_001_234_000_000,
            attrs={"pendo.span.type": "GENERATION"},
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.start_time == 1_700_000_000_000
        assert event.end_time == 1_700_000_001_234
        assert event.duration_ms == 1234.0

    def test_trace_ids_formatted(self):
        exporter = _make_exporter()
        span = _make_span(attrs={"pendo.span.type": "GENERATION"})
        event = exporter._build_event(span, dict(span.attributes))

        assert len(event.trace_id) == 32
        assert all(c in "0123456789abcdef" for c in event.trace_id)
        assert len(event.span_id) == 16

    def test_parent_span_id_mapped(self):
        exporter = _make_exporter()
        span = _make_span(
            parent=0xABCDEF1234567890,
            attrs={"pendo.span.type": "GENERATION"},
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.parent_span_id is not None
        assert len(event.parent_span_id) == 16

    def test_no_parent_span_id(self):
        exporter = _make_exporter()
        span = _make_span(attrs={"pendo.span.type": "GENERATION"})
        event = exporter._build_event(span, dict(span.attributes))

        assert event.parent_span_id is None

    def test_error_status_mapped(self):
        exporter = _make_exporter()
        span = _make_span(
            attrs={"pendo.span.type": "GENERATION"},
            status_code=StatusCode.ERROR,
            status_description="timeout connecting to LLM",
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.status == SpanStatusCode.ERROR
        assert event.error_message == "timeout connecting to LLM"

    def test_ok_status_no_error_message(self):
        exporter = _make_exporter()
        span = _make_span(attrs={"pendo.span.type": "GENERATION"})
        event = exporter._build_event(span, dict(span.attributes))

        assert event.status == SpanStatusCode.OK
        assert event.error_message is None

    def test_unknown_span_type(self):
        exporter = _make_exporter()
        span = _make_span(attrs={"pendo.span.type": "FOOBAR"})
        event = exporter._build_event(span, dict(span.attributes))

        assert event.type == SpanType.UNKNOWN

    def test_missing_span_type(self):
        exporter = _make_exporter()
        span = _make_span(attrs={})
        event = exporter._build_event(span, dict(span.attributes))

        assert event.type == SpanType.UNKNOWN

    def test_conversation_id_mapped(self):
        exporter = _make_exporter()
        span = _make_span(
            attrs={
                "pendo.span.type": "GENERATION",
                "pendo.conversation_id": "conv_abc",
            },
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.conversation_id == "conv_abc"

    def test_unknown_attrs_go_to_extra(self):
        exporter = _make_exporter()
        span = _make_span(
            attrs={
                "pendo.span.type": "GENERATION",
                "llm.model_name": "gpt-4",
                "custom.tag": "experiment-42",
                "custom.version": 3,
            },
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.props.model == ["gpt-4"]
        assert event.props.extra["custom.tag"] == "experiment-42"
        assert event.props.extra["custom.version"] == 3

    def test_total_tokens_and_cost(self):
        exporter = _make_exporter()
        span = _make_span(
            attrs={
                "pendo.span.type": "GENERATION",
                "llm.token_count.prompt": 200,
                "llm.token_count.completion": 80,
                "llm.token_count.total": 280,
                "llm.cost_usd": 0.0042,
            },
        )
        event = exporter._build_event(span, dict(span.attributes))

        assert event.props.prompt_tokens == 200
        assert event.props.completion_tokens == 80
        assert event.props.total_tokens == 280
        assert event.props.cost_usd == 0.0042

    def test_sdk_metadata(self):
        exporter = _make_exporter()
        span = _make_span(attrs={"pendo.span.type": "GENERATION"})
        event = exporter._build_event(span, dict(span.attributes))

        assert event.sdk_name == "pendo_sdk"
        assert event.sdk_version == "0.1.0"


# ---------------------------------------------------------------------------
# to_wire() output
# ---------------------------------------------------------------------------

class TestBuildEventWire:
    def test_wire_format_structure(self):
        exporter = _make_exporter()
        span = _make_span(
            name="classify",
            attrs={
                "pendo.span.type": "GENERATION",
                "llm.model_name": "claude-sonnet-4-20250514",
                "llm.token_count.prompt": 100,
            },
        )
        event = exporter._build_event(span, dict(span.attributes))
        wire = event.to_wire()

        assert wire["visitor_id"] == "user@example.com"
        assert wire["type"] == "GENERATION"
        assert wire["name"] == "classify"
        assert wire["props"]["modelUsed"] == "claude-sonnet-4-20250514"
        assert wire["props"]["modelsUsed"] == ["claude-sonnet-4-20250514"]
        assert wire["props"]["promptTokens"] == 100
        # pendo.span.type should NOT appear in props
        assert "pendo.span.type" not in wire["props"]

    def test_wire_excludes_none(self):
        exporter = _make_exporter()
        span = _make_span(attrs={"pendo.span.type": "TOOL"})
        event = exporter._build_event(span, dict(span.attributes))
        wire = event.to_wire()

        assert "parent_span_id" not in wire
        assert "error_message" not in wire
        assert "conversation_id" not in wire


# ---------------------------------------------------------------------------
# Export flow
# ---------------------------------------------------------------------------

class TestExportFlow:
    @patch("pendo_sdk.exporter.PendoSpanExporter._send_jzb")
    def test_export_success(self, mock_send):
        exporter = _make_exporter()
        spans = [_make_span(attrs={"pendo.span.type": "GENERATION"})]
        result = exporter.export(spans)

        assert result == SpanExportResult.SUCCESS
        mock_send.assert_called_once()
        wire_events = mock_send.call_args[0][0]
        assert isinstance(wire_events, list)
        assert len(wire_events) == 1

    @patch("pendo_sdk.exporter.PendoSpanExporter._send_jzb")
    def test_export_batch_format(self, mock_send):
        exporter = _make_exporter()
        spans = [
            _make_span(name="span_1", attrs={"pendo.span.type": "GENERATION"}),
            _make_span(name="span_2", attrs={"pendo.span.type": "TOOL"}),
        ]
        exporter.export(spans)

        wire_events = mock_send.call_args[0][0]
        assert len(wire_events) == 2

    @patch("pendo_sdk.exporter.PendoSpanExporter._send_jzb")
    def test_export_with_redaction(self, mock_send):
        exporter = _make_exporter(redact_pii=True)
        span = _make_span(
            attrs={
                "pendo.span.type": "GENERATION",
                "message.content": "email john@test.com",
            },
        )
        exporter.export([span])

        wire_events = mock_send.call_args[0][0]
        # GENERATION spans emit agent_trace with agentTraceContent
        trace_event = [e for e in wire_events if e["type"] == "agent_trace"][0]
        content = trace_event["props"]["agentTraceContent"]
        assert "[EMAIL_REDACTED]" in content

    @patch("pendo_sdk.exporter.PendoSpanExporter._send_jzb")
    def test_export_without_redaction(self, mock_send):
        exporter = _make_exporter(redact_pii=False)
        span = _make_span(
            attrs={
                "pendo.span.type": "GENERATION",
                "message.content": "email john@test.com",
            },
        )
        exporter.export([span])

        wire_events = mock_send.call_args[0][0]
        trace_event = [e for e in wire_events if e["type"] == "agent_trace"][0]
        content = trace_event["props"]["agentTraceContent"]
        assert "john@test.com" in content

    @patch("pendo_sdk.exporter.PendoSpanExporter._send_jzb", side_effect=Exception("boom"))
    def test_export_failure_returns_failure(self, mock_send):
        exporter = _make_exporter()
        result = exporter.export([_make_span(attrs={"pendo.span.type": "GENERATION"})])
        assert result == SpanExportResult.FAILURE

    def test_endpoint_trailing_slash_stripped(self):
        exporter = PendoSpanExporter(
            api_key="test-key",
            endpoint="https://test.pendo.io/",
        )
        assert exporter._endpoint == "https://test.pendo.io"


# ---------------------------------------------------------------------------
# Identity management
# ---------------------------------------------------------------------------

class TestIdentity:
    def test_set_identity(self):
        exporter = _make_exporter(visitor_id="old@test.com")
        exporter.set_identity(visitor_id="new@test.com", account_id="new_acct")

        assert exporter._visitor_id == "new@test.com"
        assert exporter._account_id == "new_acct"

    def test_set_identity_partial(self):
        exporter = _make_exporter(visitor_id="user@test.com", account_id="acct_1")
        exporter.set_identity(account_id="acct_2")

        assert exporter._visitor_id == "user@test.com"  # unchanged
        assert exporter._account_id == "acct_2"


# ---------------------------------------------------------------------------
# Utility
# ---------------------------------------------------------------------------

class TestNsToMs:
    def test_conversion(self):
        assert _ns_to_ms(1_000_000_000) == 1000

    def test_none(self):
        assert _ns_to_ms(None) is None

    def test_zero(self):
        assert _ns_to_ms(0) == 0
