"""Tests for the @trace_tool() decorator."""

import asyncio
from typing import List, Sequence

import pytest
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider, ReadableSpan
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter, SpanExportResult
from opentelemetry.trace import StatusCode

from pendo_sdk.decorators import trace_tool


class _InMemoryExporter(SpanExporter):
    """Minimal in-memory exporter for test assertions."""

    def __init__(self) -> None:
        self._spans: List[ReadableSpan] = []

    def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
        self._spans.extend(spans)
        return SpanExportResult.SUCCESS

    def get_finished_spans(self) -> List[ReadableSpan]:
        return list(self._spans)

    def shutdown(self) -> None:
        pass


@pytest.fixture(autouse=True)
def _otel_setup():
    """Set up an in-memory exporter so we can inspect finished spans."""
    exporter = _InMemoryExporter()
    provider = TracerProvider()
    provider.add_span_processor(SimpleSpanProcessor(exporter))
    trace.set_tracer_provider(provider)
    yield exporter
    provider.shutdown()
    trace._TRACER_PROVIDER = None
    trace._TRACER_PROVIDER_SET_ONCE._done = False


class TestTraceToolSync:
    def test_creates_tool_span(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool(name="my_tool")
        def my_func():
            return "ok"

        result = my_func()
        assert result == "ok"

        spans = exporter.get_finished_spans()
        assert len(spans) == 1
        span = spans[0]
        assert span.name == "my_tool"
        assert span.attributes["pendo.span.type"] == "TOOL"

    def test_default_name_uses_function_name(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool()
        def search_database():
            return []

        search_database()

        spans = exporter.get_finished_spans()
        assert spans[0].name == "search_database"

    def test_captures_input_arguments(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool(name="search")
        def search(query: str, limit: int = 10):
            return []

        search("hello", limit=5)

        span = exporter.get_finished_spans()[0]
        tool_input = span.attributes["tool.input"]
        assert "'query': 'hello'" in tool_input
        assert "'limit': 5" in tool_input

    def test_captures_output(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool(name="lookup")
        def lookup(key: str):
            return {"found": True, "value": 42}

        result = lookup("abc")
        assert result == {"found": True, "value": 42}

        span = exporter.get_finished_spans()[0]
        assert "found" in span.attributes["tool.output"]
        assert "42" in span.attributes["tool.output"]

    def test_handles_no_args(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool(name="ping")
        def ping():
            return "pong"

        assert ping() == "pong"

        span = exporter.get_finished_spans()[0]
        assert span.attributes["tool.input"] == "{}"
        assert span.attributes["tool.output"] == "pong"

    def test_exception_sets_error_status(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool(name="failing_tool")
        def bad_tool():
            raise ValueError("tool broke")

        with pytest.raises(ValueError, match="tool broke"):
            bad_tool()

        span = exporter.get_finished_spans()[0]
        assert span.status.status_code == StatusCode.ERROR
        assert "tool broke" in span.status.description

    def test_preserves_function_metadata(self):
        @trace_tool(name="custom")
        def my_documented_tool():
            """Tool docstring."""
            pass

        assert my_documented_tool.__name__ == "my_documented_tool"
        assert my_documented_tool.__doc__ == "Tool docstring."


class TestTraceToolAsync:
    def test_async_creates_tool_span(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool(name="async_tool")
        async def async_search(query: str):
            return [query, "result"]

        result = asyncio.run(async_search("test"))
        assert result == ["test", "result"]

        spans = exporter.get_finished_spans()
        assert len(spans) == 1
        span = spans[0]
        assert span.name == "async_tool"
        assert span.attributes["pendo.span.type"] == "TOOL"
        assert "'query': 'test'" in span.attributes["tool.input"]

    def test_async_exception_sets_error_status(self, _otel_setup):
        exporter = _otel_setup

        @trace_tool(name="async_fail")
        async def bad_async_tool():
            raise RuntimeError("async tool broke")

        with pytest.raises(RuntimeError, match="async tool broke"):
            asyncio.run(bad_async_tool())

        span = exporter.get_finished_spans()[0]
        assert span.status.status_code == StatusCode.ERROR
