"""Tests for LangfuseTraceEmitter integration."""

from __future__ import annotations

from dr_langfuse.client import ClientResolution
from dr_langfuse.errors.codes import ErrorCode
from dr_langfuse.tracing.emitter import LangfuseTraceEmitter
from dr_langfuse.tracing.models import TraceEventRequest


class _ApiErrorLike(RuntimeError):
    def __init__(self, status_code: int) -> None:
        super().__init__(f"api status {status_code}")
        self.status_code = status_code


class _ConnectErrorLike(RuntimeError):
    def __init__(self, message: str = "transport failure") -> None:
        super().__init__(message)
        self.request = object()


class _FakeLangfuseClient:
    def __init__(self) -> None:
        self.trace_flushed = False
        self._trace_id = "trace_123"

    def create_event(
        self,
        *,
        name: str,
        input: object | None = None,
        metadata: object | None = None,
    ) -> object:
        del name, input, metadata
        return object()

    def get_current_trace_id(self) -> str:
        return self._trace_id

    def flush(self) -> None:
        self.trace_flushed = True


def test_trace_emitter_success_path() -> None:
    client = _FakeLangfuseClient()
    emitter = LangfuseTraceEmitter(client=client)

    ack = emitter.emit_trace(
        TraceEventRequest(
            event_name="runtime.step",
            session_id="session-1",
            tags=["runtime"],
            metadata={"attempt": 1},
        )
    )

    assert ack.accepted is True
    assert ack.trace_id == "trace_123"
    assert ack.error is None
    assert client.trace_flushed is True


def test_trace_emitter_places_session_and_tags_in_input_payload() -> None:
    class _Client:
        def __init__(self) -> None:
            self.captured_input: object | None = None

        def create_event(
            self,
            *,
            name: str,
            metadata: object | None = None,
            input: object | None = None,
        ) -> None:
            del name, metadata
            self.captured_input = input

        def get_current_trace_id(self) -> str:
            return "trace_456"

    client = _Client()
    emitter = LangfuseTraceEmitter(client=client)
    ack = emitter.emit_trace(
        TraceEventRequest(
            event_name="runtime.step",
            session_id="session-1",
            tags=["runtime"],
        )
    )

    assert ack.accepted is True
    assert ack.trace_id == "trace_456"
    assert client.captured_input == {
        "session_id": "session-1",
        "tags": ["runtime"],
    }


def test_trace_emitter_rejects_when_trace_id_lookup_fails() -> None:
    class _Client:
        def __init__(self) -> None:
            self.create_calls = 0

        def create_event(
            self,
            *,
            name: str,
            metadata: object | None = None,
            input: object | None = None,
        ) -> None:
            del name, metadata, input
            self.create_calls += 1

        def get_current_trace_id(self) -> str:
            raise RuntimeError("trace id unavailable")

    client = _Client()
    emitter = LangfuseTraceEmitter(client=client)
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert client.create_calls == 1
    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.INTERNAL_ERROR
    assert ack.error.details.get("reason") == "missing_trace_id"


def test_trace_emitter_accepts_when_flush_fails() -> None:
    class _Client:
        def __init__(self) -> None:
            self.create_calls = 0
            self.flush_calls = 0

        def create_event(
            self,
            *,
            name: str,
            metadata: object | None = None,
            input: object | None = None,
        ) -> None:
            del name, metadata, input
            self.create_calls += 1

        def get_current_trace_id(self) -> str:
            return "trace_flush"

        def flush(self) -> None:
            self.flush_calls += 1
            raise RuntimeError("flush failed")

    client = _Client()
    emitter = LangfuseTraceEmitter(client=client)
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert client.create_calls == 1
    assert client.flush_calls == 1
    assert ack.accepted is True
    assert ack.trace_id == "trace_flush"
    assert ack.error is None


def test_trace_emitter_rejects_when_client_resolution_has_no_client() -> None:
    emitter = LangfuseTraceEmitter(client=_FakeLangfuseClient())
    emitter._client_resolution = ClientResolution.model_construct(
        client=None, init_error=None
    )
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.INTERNAL_ERROR
    assert ack.error.details.get("reason") == "client_resolution_failed"


def test_trace_emitter_rejects_clients_without_metadata_kwarg() -> None:
    class _LegacyNoMetadataClient:
        def __init__(self) -> None:
            self.calls = 0
            self.captured_input: object | None = None

        def create_event(
            self,
            *,
            name: str,
            input: object | None = None,
        ) -> None:
            del name
            self.calls += 1
            self.captured_input = input

    client = _LegacyNoMetadataClient()
    emitter = LangfuseTraceEmitter(client=client)
    ack = emitter.emit_trace(
        TraceEventRequest(
            event_name="runtime.step",
            session_id="session-1",
            tags=["runtime"],
            metadata={"attempt": 1},
        )
    )

    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.INTERNAL_ERROR


def test_trace_emitter_maps_unavailable_errors() -> None:
    class _UnavailableClient:
        def create_event(
            self,
            *,
            name: str,
            input: object | None = None,
            metadata: object | None = None,
        ) -> None:
            del name, input, metadata
            raise _ApiErrorLike(503)

    emitter = LangfuseTraceEmitter(client=_UnavailableClient())
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.UNAVAILABLE
    assert ack.error.retriable is True


def test_trace_emitter_maps_transport_errors_to_unavailable() -> None:
    class _UnavailableClient:
        def create_event(
            self,
            *,
            name: str,
            input: object | None = None,
            metadata: object | None = None,
        ) -> None:
            del name, input, metadata
            raise _ConnectErrorLike()

    emitter = LangfuseTraceEmitter(client=_UnavailableClient())
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.UNAVAILABLE
    assert ack.error.retriable is True


def test_trace_emitter_rejects_client_without_create_event() -> None:
    class _TraceOnlyClient:
        def trace(
            self,
            *,
            name: str,
            session_id: str | None = None,
            tags: list[str] | None = None,
            metadata: dict[str, object] | None = None,
        ) -> object:
            del name, session_id, tags, metadata
            return object()

    emitter = LangfuseTraceEmitter(client=_TraceOnlyClient())
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.INTERNAL_ERROR


def test_trace_emitter_maps_internal_errors() -> None:
    class _InternalClient:
        def create_event(
            self,
            *,
            name: str,
            input: object | None = None,
            metadata: object | None = None,
        ) -> None:
            del name, input, metadata
            raise RuntimeError("unexpected schema mismatch")

    emitter = LangfuseTraceEmitter(client=_InternalClient())
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.INTERNAL_ERROR


def test_trace_emitter_does_not_misclassify_service_schema_errors() -> None:
    class _Client:
        def create_event(
            self,
            *,
            name: str,
            input: object | None = None,
            metadata: object | None = None,
        ) -> None:
            del name, input, metadata
            raise RuntimeError("service response schema mismatch")

    emitter = LangfuseTraceEmitter(client=_Client())
    ack = emitter.emit_trace(TraceEventRequest(event_name="runtime.step"))

    assert ack.accepted is False
    assert ack.error is not None
    assert ack.error.code == ErrorCode.INTERNAL_ERROR
