"""Tests for the Flowlines SpanExporter."""

import logging
from unittest.mock import MagicMock, patch

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

from flowlines._exporter import FlowlinesExporter


def _make_span(
    name: str,
    attributes: dict[str, str | bool | int | float] | None = None,
) -> ReadableSpan:
    """Create a ReadableSpan with the given attributes."""
    return ReadableSpan(name=name, attributes=attributes)


class TestFlowlinesExporter:
    @patch(
        "flowlines._exporter.OTLPSpanExporter",
        autospec=True,
    )
    def test_export_all_llm_spans(self, mock_otlp_cls: MagicMock) -> None:
        """All LLM spans are forwarded to the inner exporter."""
        mock_inner = mock_otlp_cls.return_value
        mock_inner.export.return_value = SpanExportResult.SUCCESS

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [
            _make_span("s1", {"gen_ai.system": "openai"}),
            _make_span("s2", {"ai.pipeline": "p"}),
        ]
        result = exporter.export(spans)

        assert result == SpanExportResult.SUCCESS
        mock_inner.export.assert_called_once()
        exported = mock_inner.export.call_args[0][0]
        assert len(exported) == 2

    @patch(
        "flowlines._exporter.OTLPSpanExporter",
        autospec=True,
    )
    def test_export_mixed_batch(self, mock_otlp_cls: MagicMock) -> None:
        """Only LLM spans are forwarded from a mixed batch."""
        mock_inner = mock_otlp_cls.return_value
        mock_inner.export.return_value = SpanExportResult.SUCCESS

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [
            _make_span("llm", {"gen_ai.system": "openai"}),
            _make_span("http", {"http.method": "GET"}),
            _make_span("llm2", {"ai.tool": "t"}),
        ]
        result = exporter.export(spans)

        assert result == SpanExportResult.SUCCESS
        mock_inner.export.assert_called_once()
        exported = mock_inner.export.call_args[0][0]
        assert len(exported) == 2

    @patch(
        "flowlines._exporter.OTLPSpanExporter",
        autospec=True,
    )
    def test_export_no_llm_spans(self, mock_otlp_cls: MagicMock) -> None:
        """No network request when all spans are non-LLM."""
        mock_inner = mock_otlp_cls.return_value

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [
            _make_span("http", {"http.method": "GET"}),
            _make_span("db", {"db.system": "postgres"}),
        ]
        result = exporter.export(spans)

        assert result == SpanExportResult.SUCCESS
        mock_inner.export.assert_not_called()

    @patch(
        "flowlines._exporter.OTLPSpanExporter",
        autospec=True,
    )
    def test_shutdown_delegates(self, mock_otlp_cls: MagicMock) -> None:
        """shutdown() delegates to the inner exporter."""
        mock_inner = mock_otlp_cls.return_value

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        exporter.shutdown()

        mock_inner.shutdown.assert_called_once()

    @patch(
        "flowlines._exporter.OTLPSpanExporter",
        autospec=True,
    )
    def test_force_flush_delegates(self, mock_otlp_cls: MagicMock) -> None:
        """force_flush() delegates to the inner exporter."""
        mock_inner = mock_otlp_cls.return_value
        mock_inner.force_flush.return_value = True

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        result = exporter.force_flush()

        assert result is True
        mock_inner.force_flush.assert_called_once_with(30000)

    @patch(
        "flowlines._exporter.OTLPSpanExporter",
        autospec=True,
    )
    def test_constructor_configures_otlp(self, mock_otlp_cls: MagicMock) -> None:
        """Constructor passes correct config to OTLPSpanExporter."""
        from opentelemetry.exporter.otlp.proto.http import Compression

        FlowlinesExporter(
            api_key="my-key",
            endpoint="https://api.flowlines.io",
        )

        mock_otlp_cls.assert_called_once_with(
            endpoint="https://api.flowlines.io/v1/traces",
            compression=Compression.Gzip,
            headers={"x-flowlines-api-key": "my-key"},
        )

    @patch(
        "flowlines._exporter.OTLPSpanExporter",
        autospec=True,
    )
    def test_trailing_slash_stripped(self, mock_otlp_cls: MagicMock) -> None:
        """Trailing slash on endpoint does not produce a double-slash URL."""
        from opentelemetry.exporter.otlp.proto.http import Compression

        FlowlinesExporter(
            api_key="key",
            endpoint="https://api.flowlines.io/",
        )

        mock_otlp_cls.assert_called_once_with(
            endpoint="https://api.flowlines.io/v1/traces",
            compression=Compression.Gzip,
            headers={"x-flowlines-api-key": "key"},
        )


class TestMissingContextWarning:
    """Tests for the warning when LLM spans are missing flowlines context."""

    @patch("flowlines._exporter.OTLPSpanExporter", autospec=True)
    def test_warning_logged_when_context_missing(
        self, mock_otlp_cls: MagicMock, caplog: logging.LogCaptureFixture
    ) -> None:
        """A warning is logged when an LLM span lacks flowlines context attributes."""
        mock_otlp_cls.return_value.export.return_value = SpanExportResult.SUCCESS

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [_make_span("llm", {"gen_ai.system": "openai"})]

        with caplog.at_level(logging.WARNING, logger="flowlines"):
            exporter.export(spans)

        assert any("flowlines.user_id" in msg for msg in caplog.messages)

    @patch("flowlines._exporter.OTLPSpanExporter", autospec=True)
    def test_warning_logged_only_once(
        self, mock_otlp_cls: MagicMock, caplog: logging.LogCaptureFixture
    ) -> None:
        """The warning is emitted only once across multiple export calls."""
        mock_otlp_cls.return_value.export.return_value = SpanExportResult.SUCCESS

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [_make_span("llm", {"gen_ai.system": "openai"})]

        with caplog.at_level(logging.WARNING, logger="flowlines"):
            exporter.export(spans)
            exporter.export(spans)
            exporter.export(spans)

        warning_msgs = [msg for msg in caplog.messages if "flowlines.user_id" in msg]
        assert len(warning_msgs) == 1

    @patch("flowlines._exporter.OTLPSpanExporter", autospec=True)
    def test_no_warning_when_context_present(
        self, mock_otlp_cls: MagicMock, caplog: logging.LogCaptureFixture
    ) -> None:
        """No warning when LLM spans have both flowlines context attributes."""
        mock_otlp_cls.return_value.export.return_value = SpanExportResult.SUCCESS

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [
            _make_span(
                "llm",
                {
                    "gen_ai.system": "openai",
                    "flowlines.user_id": "user-1",
                    "flowlines.session_id": "sess-1",
                },
            ),
        ]

        with caplog.at_level(logging.WARNING, logger="flowlines"):
            exporter.export(spans)

        warning_msgs = [msg for msg in caplog.messages if "flowlines.user_id" in msg]
        assert len(warning_msgs) == 0

    @patch("flowlines._exporter.OTLPSpanExporter", autospec=True)
    def test_warning_when_user_id_missing(
        self, mock_otlp_cls: MagicMock, caplog: logging.LogCaptureFixture
    ) -> None:
        """A warning is logged when session_id is present but user_id is missing."""
        mock_otlp_cls.return_value.export.return_value = SpanExportResult.SUCCESS

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [
            _make_span(
                "llm",
                {"gen_ai.system": "openai", "flowlines.session_id": "sess-1"},
            ),
        ]

        with caplog.at_level(logging.WARNING, logger="flowlines"):
            exporter.export(spans)

        assert any("flowlines.user_id" in msg for msg in caplog.messages)

    @patch("flowlines._exporter.OTLPSpanExporter", autospec=True)
    def test_warning_when_session_id_missing(
        self, mock_otlp_cls: MagicMock, caplog: logging.LogCaptureFixture
    ) -> None:
        """A warning is logged when user_id is present but session_id is missing."""
        mock_otlp_cls.return_value.export.return_value = SpanExportResult.SUCCESS

        exporter = FlowlinesExporter(api_key="key", endpoint="https://example.com")
        spans = [
            _make_span(
                "llm",
                {"gen_ai.system": "openai", "flowlines.user_id": "user-1"},
            ),
        ]

        with caplog.at_level(logging.WARNING, logger="flowlines"):
            exporter.export(spans)

        assert any("flowlines.session_id" in msg for msg in caplog.messages)
