"""Tests for :mod:`mgf.common.observability.otel_setup`.

Uses :class:`opentelemetry.sdk.trace.export.in_memory_span_exporter.InMemorySpanExporter`
to capture emitted spans deterministically without needing a real
OTLP collector. The session-scoped fixture in ``conftest.py`` builds
ONE TracerProvider for the whole pytest session and exposes the
exporter via the per-test ``in_memory_spans`` fixture (which clears
the buffer between tests).
"""

from __future__ import annotations

from typing import Any

import pytest

from mgf.common.observability import operation_span
from mgf.common.observability import otel_setup as otel_setup_mod
from mgf.common.observability.json_formatter import JsonFormatter
from mgf.common.observability.otel_setup import _wire_otel
from mgf.common.observability.redaction import Redactor

# ---------------------------------------------------------------------------
# operation_span — no-op path (SDK present but no provider initialised)
# ---------------------------------------------------------------------------


class TestNoOp:
    def test_operation_span_yields_without_active_provider(self) -> None:
        """When OTel is uninitialised, operation_span MUST run the
        body without opening a real span (zero-overhead path)."""
        ran = False
        with operation_span("test.op", foo="bar"):
            ran = True
        assert ran

    def test_operation_span_propagates_exception_when_no_provider(self) -> None:
        with pytest.raises(ValueError, match="boom"), operation_span("test.op"):
            raise ValueError("boom")


# ---------------------------------------------------------------------------
# operation_span — real spans via in-memory exporter
# ---------------------------------------------------------------------------


class TestRealSpans:
    def test_operation_span_emits_a_span(self, in_memory_spans: Any) -> None:
        with operation_span("test.simple"):
            pass
        spans = in_memory_spans.get_finished_spans()
        assert len(spans) == 1
        assert spans[0].name == "test.simple"

    def test_attributes_are_attached(self, in_memory_spans: Any) -> None:
        with operation_span("vm.test", vm_name="prod-1", count=3, force=True):
            pass
        spans = in_memory_spans.get_finished_spans()
        assert spans[0].attributes["vm_name"] == "prod-1"
        assert spans[0].attributes["count"] == 3
        assert spans[0].attributes["force"] is True

    def test_none_attributes_are_skipped(self, in_memory_spans: Any) -> None:
        """``operation_span`` skips ``None`` values because the OTel
        attribute API rejects them. Without this guard, callers that
        pass an optional like ``new_name=None`` would crash the span."""
        with operation_span("vm.test", vm_name="prod-1", new_name=None):
            pass
        attrs = in_memory_spans.get_finished_spans()[0].attributes
        assert attrs["vm_name"] == "prod-1"
        assert "new_name" not in attrs

    def test_exception_records_on_span(self, in_memory_spans: Any) -> None:
        from opentelemetry.trace import StatusCode

        with pytest.raises(ValueError, match="kaboom"):  # noqa: SIM117
            with operation_span("test.failing"):
                raise ValueError("kaboom")

        span = in_memory_spans.get_finished_spans()[0]
        assert span.status.status_code == StatusCode.ERROR
        # The exception event is recorded with the type + message.
        events = list(span.events)
        assert any(e.name == "exception" for e in events)
        exc_event = next(e for e in events if e.name == "exception")
        assert "ValueError" in exc_event.attributes["exception.type"]
        assert "kaboom" in exc_event.attributes["exception.message"]

    def test_nested_spans_form_parent_child(self, in_memory_spans: Any) -> None:
        with operation_span("outer"), operation_span("inner"):
            pass
        # Spans are exported in finish order — children finish before
        # parents — so inner is at index 0, outer at index 1.
        spans = in_memory_spans.get_finished_spans()
        inner = next(s for s in spans if s.name == "inner")
        outer = next(s for s in spans if s.name == "outer")
        assert inner.parent is not None
        assert inner.parent.span_id == outer.context.span_id
        assert inner.context.trace_id == outer.context.trace_id


# ---------------------------------------------------------------------------
# Trace context flows into the JSON log formatter
# ---------------------------------------------------------------------------


class TestTraceContextOnLogs:
    def test_trace_id_appears_on_log_record_inside_span(self, in_memory_spans: Any) -> None:
        import json
        import logging

        formatter = JsonFormatter(redactor=Redactor.default())
        rec_payload: dict[str, Any] = {}
        with operation_span("test.logged"):
            rec = logging.LogRecord(
                name="test",
                level=logging.INFO,
                pathname="x.py",
                lineno=1,
                msg="inside span",
                args=None,
                exc_info=None,
            )
            rec_payload = json.loads(formatter.format(rec))
        # While the span was active, the formatter saw it and
        # attached trace_id + span_id.
        assert "trace_id" in rec_payload
        assert "span_id" in rec_payload
        # 32 hex chars for trace, 16 for span (per OTel spec).
        assert len(rec_payload["trace_id"]) == 32
        assert len(rec_payload["span_id"]) == 16


# ---------------------------------------------------------------------------
# setup_otel idempotency + opt-in
# ---------------------------------------------------------------------------


class TestSetupOtel:
    def test_setup_otel_noop_when_endpoint_unset(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
        # _INSTALLED is the idempotency flag — reset it so the
        # function actually runs the body.
        monkeypatch.setattr(otel_setup_mod, "_INSTALLED", False)
        _wire_otel()
        # When endpoint is unset, the function returns without
        # initialising. _INSTALLED stays False.
        assert otel_setup_mod._INSTALLED is False

    def test_setup_otel_idempotent(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
        monkeypatch.setattr(otel_setup_mod, "_INSTALLED", True)
        # Pre-trip the RA-16 re-wire warning guard so the
        # idempotency path doesn't ALSO trigger the
        # MgfPlatformLimitation warning. RA-16's behavior is
        # tested in TestOtelRewireLimitation; this test is only
        # asserting the idempotency invariant.
        monkeypatch.setattr(otel_setup_mod, "_REWIRE_WARNED", True)
        # When already installed, the function returns immediately
        # regardless of the endpoint var.
        _wire_otel(enabled=True)  # would normally try to install
        # Did not raise; _INSTALLED unchanged.
        assert otel_setup_mod._INSTALLED is True

    def test_setup_otel_explicit_disable_overrides_env(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
        monkeypatch.setattr(otel_setup_mod, "_INSTALLED", False)
        _wire_otel(enabled=False)
        # enabled=False wins over the env var → no install.
        assert otel_setup_mod._INSTALLED is False


class TestOtelRewireLimitation:
    """RA-16: ``trace.set_tracer_provider`` is one-shot per process.
    A second call to ``_wire_otel`` with ``enabled=True`` cannot
    install a new TracerProvider; the first call's config is
    permanent. Emit a one-shot
    :class:`MgfPlatformLimitation` warning so operators know."""

    def test_warning_fires_on_second_wire_with_enabled(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        import warnings as _warnings

        from mgf.common._warnings import MgfPlatformLimitation
        from mgf.common.observability import otel_setup as _otel_mod

        # Simulate "first bootstrap already installed OTel".
        monkeypatch.setattr(_otel_mod, "_INSTALLED", True)
        # Reset the one-shot warning guard so this test is
        # independent of any earlier test having tripped it.
        monkeypatch.setattr(_otel_mod, "_REWIRE_WARNED", False)

        with _warnings.catch_warnings(record=True) as caught:
            _warnings.simplefilter("always")
            _otel_mod._wire_otel(enabled=True)

        platform_warnings = [w for w in caught if issubclass(w.category, MgfPlatformLimitation)]
        assert len(platform_warnings) == 1
        msg = str(platform_warnings[0].message)
        assert "one-shot" in msg
        assert "set_tracer_provider" in msg
        assert "IGNORED" in msg

    def test_warning_is_one_shot(self, monkeypatch: pytest.MonkeyPatch) -> None:
        import warnings as _warnings

        from mgf.common._warnings import MgfPlatformLimitation
        from mgf.common.observability import otel_setup as _otel_mod

        monkeypatch.setattr(_otel_mod, "_INSTALLED", True)
        monkeypatch.setattr(_otel_mod, "_REWIRE_WARNED", False)

        # First re-wire call: emits.
        with _warnings.catch_warnings(record=True) as first:
            _warnings.simplefilter("always")
            _otel_mod._wire_otel(enabled=True)
        assert sum(1 for w in first if issubclass(w.category, MgfPlatformLimitation)) == 1

        # Second re-wire call: silent.
        with _warnings.catch_warnings(record=True) as second:
            _warnings.simplefilter("always")
            _otel_mod._wire_otel(enabled=True)
        assert sum(1 for w in second if issubclass(w.category, MgfPlatformLimitation)) == 0

    def test_no_warning_when_second_call_disabled(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """If the second call has ``enabled=False`` (the consumer
        explicitly opted out), no warning — they were never going
        to install anything."""
        import warnings as _warnings

        from mgf.common._warnings import MgfPlatformLimitation
        from mgf.common.observability import otel_setup as _otel_mod

        monkeypatch.setattr(_otel_mod, "_INSTALLED", True)
        monkeypatch.setattr(_otel_mod, "_REWIRE_WARNED", False)
        # Make sure the env var doesn't accidentally enable it.
        monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)

        with _warnings.catch_warnings(record=True) as caught:
            _warnings.simplefilter("always")
            _otel_mod._wire_otel(enabled=False)

        platform_warnings = [w for w in caught if issubclass(w.category, MgfPlatformLimitation)]
        assert platform_warnings == []
