import asyncio
import os
from pathlib import Path

import pytest

from cascade import tracing
from cascade.tool_decorator import tool
import cascade.tool_decorator as tool_module


def _ensure_tracing_initialized() -> None:
    if tracing.get_tracer() is None:
        api_key = os.getenv("CASCADE_API_KEY")
        if not api_key:
            env_path = Path(__file__).resolve().parents[1] / ".env"
            if env_path.exists():
                for line in env_path.read_text(encoding="utf-8").splitlines():
                    if line.startswith("CASCADE_API_KEY="):
                        api_key = line.split("=", 1)[1].strip()
                        break
        tracing.init_tracing(
            project="pytest_tracing_tool_edges",
            endpoint="http://localhost:8000/v1/traces",
            api_key=api_key or "test_key",
        )


@pytest.fixture(autouse=True)
def _clear_session_context():
    tracing.set_session_id(None)
    yield
    tracing.set_session_id(None)


def test_trace_run_raises_when_not_initialized(monkeypatch):
    monkeypatch.setattr(tracing, "_tracer", None)

    with pytest.raises(RuntimeError, match="Tracing not initialized"):
        with tracing.trace_run("NoInit"):
            pass


def test_trace_run_inherits_session_id_from_set_session_id(monkeypatch):
    _ensure_tracing_initialized()
    captured = {}

    def fake_send_trace_start(**kwargs):
        captured.update(kwargs)

    monkeypatch.setattr(tracing, "_send_trace_start", fake_send_trace_start)
    tracing.set_session_id("session-from-context")

    with tracing.trace_run("InheritedSession"):
        pass

    assert captured["session_id"] == "session-from-context"
    assert captured["metadata"]["session_id"] == "session-from-context"


def test_trace_run_honors_explicit_session_id_over_context(monkeypatch):
    _ensure_tracing_initialized()
    captured = {}

    def fake_send_trace_start(**kwargs):
        captured.update(kwargs)

    monkeypatch.setattr(tracing, "_send_trace_start", fake_send_trace_start)
    tracing.set_session_id("context-session")

    with tracing.trace_run("OverrideSession", metadata={"session_id": "explicit-session"}):
        pass

    assert captured["session_id"] == "explicit-session"
    assert captured["metadata"]["session_id"] == "explicit-session"


def test_trace_session_scopes_and_restores_session_id(monkeypatch):
    _ensure_tracing_initialized()
    captured_sessions = []

    def fake_send_trace_start(**kwargs):
        captured_sessions.append(kwargs["session_id"])

    monkeypatch.setattr(tracing, "_send_trace_start", fake_send_trace_start)
    tracing.set_session_id("base-session")

    with tracing.trace_session("inner-session"):
        with tracing.trace_run("InsideSession"):
            pass

    with tracing.trace_run("OutsideSession"):
        pass

    assert captured_sessions == ["inner-session", "base-session"]


class _FakeSpan:
    def __init__(self, name: str):
        self.name = name
        self.attributes = {}
        self.status = None
        self.exceptions = []

    def set_attribute(self, key, value):
        self.attributes[key] = value

    def set_status(self, status):
        self.status = status

    def record_exception(self, exc):
        self.exceptions.append(exc)


class _FakeSpanContext:
    def __init__(self, tracer, span):
        self._tracer = tracer
        self._span = span

    def __enter__(self):
        self._tracer.current_span = self._span
        return self._span

    def __exit__(self, exc_type, exc, tb):
        self._tracer.current_span = None
        return False


class _FakeTracer:
    def __init__(self):
        self.spans = []
        self.current_span = None

    def start_as_current_span(self, name, kind=None):
        span = _FakeSpan(name)
        self.spans.append(span)
        return _FakeSpanContext(self, span)


def _patch_fake_tracer(monkeypatch):
    fake_tracer = _FakeTracer()
    monkeypatch.setattr(tool_module, "get_tracer", lambda: fake_tracer)
    monkeypatch.setattr(tool_module.trace, "get_current_span", lambda: fake_tracer.current_span)
    return fake_tracer


def test_tool_executes_without_tracer(monkeypatch):
    monkeypatch.setattr(tool_module, "get_tracer", lambda: None)

    @tool
    def add(a, b):
        return a + b

    assert add(2, 3) == 5


def test_tool_exception_propagates_and_marks_error(monkeypatch):
    fake_tracer = _patch_fake_tracer(monkeypatch)

    @tool
    def explode():
        raise ValueError("boom")

    with pytest.raises(ValueError, match="boom"):
        explode()

    span = fake_tracer.spans[-1]
    assert span.attributes["tool.error"] == "boom"
    assert span.status.status_code.name == "ERROR"
    assert isinstance(span.exceptions[-1], ValueError)


def test_tool_handles_input_serialization_failure(monkeypatch):
    fake_tracer = _patch_fake_tracer(monkeypatch)

    class BadStr:
        def __str__(self):
            raise RuntimeError("cannot stringify")

    @tool
    def returns_ok(value):
        return {"ok": True}

    result = returns_ok(BadStr())
    span = fake_tracer.spans[-1]

    assert result == {"ok": True}
    assert "<error serializing:" in span.attributes["tool.input"]
    assert span.status.status_code.name == "OK"


def test_async_tool_success_and_failure_paths(monkeypatch):
    fake_tracer = _patch_fake_tracer(monkeypatch)

    @tool
    async def async_add(a, b):
        return a + b

    @tool
    async def async_fail():
        raise RuntimeError("async boom")

    assert asyncio.run(async_add(1, 2)) == 3

    with pytest.raises(RuntimeError, match="async boom"):
        asyncio.run(async_fail())

    success_span = fake_tracer.spans[-2]
    failure_span = fake_tracer.spans[-1]

    assert success_span.status.status_code.name == "OK"
    assert failure_span.attributes["tool.error"] == "async boom"
    assert failure_span.status.status_code.name == "ERROR"
