from __future__ import annotations

import logging
from uuid import uuid4

import agents.tracing.traces as trace_module
from agents.tracing import TracingConfig, set_tracing_disabled, trace
from agents.tracing.context import create_trace_for_run
from agents.tracing.scope import Scope
from agents.tracing.traces import (
    NoOpTrace,
    ReattachedTrace,
    TraceImpl,
    TraceState,
    _started_trace_ids,
    _started_trace_ids_lock,
)


def _new_trace_id() -> str:
    return f"trace_{uuid4().hex}"


def _clear_started_trace_ids() -> None:
    with _started_trace_ids_lock:
        _started_trace_ids.clear()


def _mark_trace_as_started(
    *,
    workflow_name: str = "workflow",
    group_id: str | None = "group-1",
    metadata: dict[str, str] | None = None,
    tracing_api_key: str | None = None,
) -> TraceState:
    metadata = metadata or {"key": "value"}
    trace_id = _new_trace_id()
    Scope.set_current_trace(None)
    set_tracing_disabled(False)

    original = trace(
        workflow_name=workflow_name,
        trace_id=trace_id,
        group_id=group_id,
        metadata=metadata,
        tracing={"api_key": tracing_api_key} if tracing_api_key is not None else None,
    )
    assert isinstance(original, TraceImpl)
    original.start()
    original.finish()

    trace_state = TraceState.from_trace(original)
    assert trace_state is not None
    return trace_state


def test_create_trace_for_run_reattaches_matching_started_trace() -> None:
    trace_state = _mark_trace_as_started(tracing_api_key="trace-key")

    created = create_trace_for_run(
        workflow_name="workflow",
        trace_id=trace_state.trace_id,
        group_id=trace_state.group_id,
        metadata=dict(trace_state.metadata or {}),
        tracing={"api_key": "trace-key"},
        disabled=False,
        trace_state=trace_state,
        reattach_resumed_trace=True,
    )

    assert isinstance(created, ReattachedTrace)
    assert created.trace_id == trace_state.trace_id


def test_create_trace_for_run_does_not_reattach_after_trace_state_reload() -> None:
    trace_state = _mark_trace_as_started()
    _clear_started_trace_ids()

    created = create_trace_for_run(
        workflow_name="workflow",
        trace_id=trace_state.trace_id,
        group_id=trace_state.group_id,
        metadata=dict(trace_state.metadata or {}),
        tracing=None,
        disabled=False,
        trace_state=trace_state,
        reattach_resumed_trace=True,
    )

    assert isinstance(created, TraceImpl)
    assert not isinstance(created, ReattachedTrace)


def test_create_trace_for_run_does_not_reattach_when_trace_id_differs() -> None:
    trace_state = _mark_trace_as_started()

    created = create_trace_for_run(
        workflow_name="workflow",
        trace_id=_new_trace_id(),
        group_id=trace_state.group_id,
        metadata=dict(trace_state.metadata or {}),
        tracing=None,
        disabled=False,
        trace_state=trace_state,
        reattach_resumed_trace=True,
    )

    assert isinstance(created, TraceImpl)
    assert not isinstance(created, ReattachedTrace)


def test_create_trace_for_run_reattaches_stripped_trace_key_with_matching_resume_key() -> None:
    trace_state = _mark_trace_as_started(tracing_api_key="trace-key")
    stripped_trace_state = TraceState.from_json(trace_state.to_json())
    assert stripped_trace_state is not None
    assert stripped_trace_state.tracing_api_key is None
    assert stripped_trace_state.tracing_api_key_hash == trace_state.tracing_api_key_hash

    created = create_trace_for_run(
        workflow_name="workflow",
        trace_id=stripped_trace_state.trace_id,
        group_id=stripped_trace_state.group_id,
        metadata=dict(stripped_trace_state.metadata or {}),
        tracing={"api_key": "trace-key"},
        disabled=False,
        trace_state=stripped_trace_state,
        reattach_resumed_trace=True,
    )

    assert isinstance(created, ReattachedTrace)
    assert created.tracing_api_key == "trace-key"


def test_create_trace_for_run_does_not_reattach_stripped_trace_key_with_mismatch() -> None:
    trace_state = _mark_trace_as_started(tracing_api_key="trace-key")
    stripped_trace_state = TraceState.from_json(trace_state.to_json())
    assert stripped_trace_state is not None

    created = create_trace_for_run(
        workflow_name="workflow",
        trace_id=stripped_trace_state.trace_id,
        group_id=stripped_trace_state.group_id,
        metadata=dict(stripped_trace_state.metadata or {}),
        tracing={"api_key": "other-trace-key"},
        disabled=False,
        trace_state=stripped_trace_state,
        reattach_resumed_trace=True,
    )

    assert isinstance(created, TraceImpl)
    assert not isinstance(created, ReattachedTrace)


def test_create_trace_for_run_does_not_reattach_when_settings_mismatch() -> None:
    trace_state = _mark_trace_as_started(tracing_api_key="trace-key")

    mismatch_cases: list[tuple[str, str | None, dict[str, str], TracingConfig]] = [
        (
            "workflow-override",
            trace_state.group_id,
            dict(trace_state.metadata or {}),
            {"api_key": "trace-key"},
        ),
        (
            "workflow",
            "group-override",
            dict(trace_state.metadata or {}),
            {"api_key": "trace-key"},
        ),
        (
            "workflow",
            trace_state.group_id,
            {"key": "override"},
            {"api_key": "trace-key"},
        ),
        (
            "workflow",
            trace_state.group_id,
            dict(trace_state.metadata or {}),
            {"api_key": "other-trace-key"},
        ),
    ]

    for workflow_name, group_id, metadata, tracing in mismatch_cases:
        Scope.set_current_trace(None)
        created = create_trace_for_run(
            workflow_name=workflow_name,
            trace_id=trace_state.trace_id,
            group_id=group_id,
            metadata=metadata,
            tracing=tracing,
            disabled=False,
            trace_state=trace_state,
            reattach_resumed_trace=True,
        )

        assert isinstance(created, TraceImpl)
        assert not isinstance(created, ReattachedTrace)


def test_create_trace_for_run_respects_disabled_flag_for_resume() -> None:
    trace_state = _mark_trace_as_started()

    created = create_trace_for_run(
        workflow_name="workflow",
        trace_id=trace_state.trace_id,
        group_id=trace_state.group_id,
        metadata=dict(trace_state.metadata or {}),
        tracing=None,
        disabled=True,
        trace_state=trace_state,
        reattach_resumed_trace=True,
    )

    assert isinstance(created, NoOpTrace)


def test_create_trace_for_run_uses_existing_current_trace() -> None:
    trace_state = _mark_trace_as_started()
    outer_trace = trace(workflow_name="outer", trace_id=_new_trace_id())
    assert isinstance(outer_trace, TraceImpl)

    with outer_trace:
        created = create_trace_for_run(
            workflow_name="workflow",
            trace_id=trace_state.trace_id,
            group_id=trace_state.group_id,
            metadata=dict(trace_state.metadata or {}),
            tracing=None,
            disabled=False,
            trace_state=trace_state,
            reattach_resumed_trace=True,
        )

        assert created is None


def test_trace_logs_warning_when_current_trace_exists(
    caplog,
) -> None:
    Scope.set_current_trace(None)
    outer_trace = trace(workflow_name="outer", trace_id=_new_trace_id())
    assert isinstance(outer_trace, TraceImpl)

    with outer_trace:
        with caplog.at_level(logging.WARNING, logger="openai.agents"):
            inner_trace = trace(workflow_name="inner", trace_id=_new_trace_id())

    assert isinstance(inner_trace, TraceImpl)
    assert "Trace already exists" in caplog.text


def test_started_trace_id_cache_is_bounded(monkeypatch) -> None:
    _clear_started_trace_ids()
    monkeypatch.setattr(trace_module, "_MAX_STARTED_TRACE_IDS", 2)

    first = _mark_trace_as_started(metadata={"key": "first"})
    second = _mark_trace_as_started(metadata={"key": "second"})
    third = _mark_trace_as_started(metadata={"key": "third"})

    assert len(_started_trace_ids) == 2
    assert list(_started_trace_ids) == [second.trace_id, third.trace_id]
    assert first.trace_id not in _started_trace_ids
