import pytest
from typing import cast, Any, TypedDict


class _TracerConfig(TypedDict):
    endpoint: str
    insecure: bool
    service_name: str
    service_namespace: str


@pytest.fixture(autouse=True)
def _reset_tracer_handle_singleton(monkeypatch: pytest.MonkeyPatch):
    """Ensure tests are isolated from global singleton state."""

    import frisk_sdk.telemetry.tracer_handle as module

    # Reset between tests so we never leak global state.
    with module.TracerHandle._singleton_lock:
        module.TracerHandle._singleton = None

    yield

    # Best-effort cleanup: if a handle exists and is still alive, shut it down.
    inst = module.TracerHandle._singleton
    if inst is not None:
        try:
            inst.shutdown()
        except Exception:
            pass

    with module.TracerHandle._singleton_lock:
        module.TracerHandle._singleton = None


class _FakeOTLPSpanExporter:
    def __init__(self, *, endpoint: str, insecure: bool, headers):
        self.endpoint = endpoint
        self.insecure = insecure
        # Most common internal storage.
        self._headers = headers


class _FakeOTLPSpanExporterHeadersProp:
    """Variant where exporter stores headers in a `headers` attribute."""

    def __init__(self, *, endpoint: str, insecure: bool, headers):
        self.endpoint = endpoint
        self.insecure = insecure
        self.headers = headers


class _FakeTracerProvider:
    def __init__(self, *, resource=None):
        self.resource = resource
        self.span_processors = []
        self.shutdown_called = 0
        self.force_flush_called = 0
        self.force_flush_return = True

    def add_span_processor(self, processor):
        self.span_processors.append(processor)

    def shutdown(self):
        self.shutdown_called += 1

    def force_flush(self):
        self.force_flush_called += 1
        return self.force_flush_return


class _FakeBatchSpanProcessor:
    def __init__(self, exporter):
        self.exporter = exporter


class _FakeResource:
    @staticmethod
    def create(attrs):
        # Don't pull in OpenTelemetry SDK internals; just record attributes.
        return {"attrs": dict(attrs)}


def _make_config() -> _TracerConfig:
    return {
        "endpoint": "http://collector:4317",
        "insecure": True,
        "service_name": "svc",
        "service_namespace": "ns",
    }


@pytest.mark.unit
def test_get_instance_creates_singleton_and_sets_global_provider(
    monkeypatch: pytest.MonkeyPatch,
):
    import frisk_sdk.telemetry.tracer_handle as module

    set_tracer_provider_calls = []

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)

    monkeypatch.setattr(
        module.trace,
        "set_tracer_provider",
        lambda provider: set_tracer_provider_calls.append(provider),
    )

    cfg = _make_config()
    h = module.TracerHandle.get_instance("t1", **cfg)

    assert h is module.TracerHandle._singleton
    assert h._exporter is not None
    assert h._provider is not None

    exporter = cast(Any, h._exporter)
    assert exporter.endpoint == cfg["endpoint"]
    assert exporter.insecure == cfg["insecure"]
    assert exporter._headers == [("authorization", "Bearer t1")]

    assert len(set_tracer_provider_calls) == 1
    assert set_tracer_provider_calls[0] is h._provider


@pytest.mark.unit
def test_get_instance_returns_same_singleton_and_updates_token(
    monkeypatch: pytest.MonkeyPatch,
):
    import frisk_sdk.telemetry.tracer_handle as module

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()

    h1 = module.TracerHandle.get_instance("t1", **cfg)
    h2 = module.TracerHandle.get_instance("t2", **cfg)

    assert h1 is h2
    assert h1._exporter is not None
    assert h1._exporter._headers == [("authorization", "Bearer t2")]


@pytest.mark.unit
def test_get_instance_raises_on_config_mismatch(monkeypatch: pytest.MonkeyPatch):
    import frisk_sdk.telemetry.tracer_handle as module

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()
    module.TracerHandle.get_instance("t1", **cfg)

    bad_cfg: _TracerConfig = {
        "endpoint": "http://other:4317",
        "insecure": cfg["insecure"],
        "service_name": cfg["service_name"],
        "service_namespace": cfg["service_namespace"],
    }

    with pytest.raises(RuntimeError, match="different configuration"):
        module.TracerHandle.get_instance("t2", **bad_cfg)


@pytest.mark.unit
def test_acquire_increments_refcount_and_release_shuts_down_only_at_zero(
    monkeypatch: pytest.MonkeyPatch,
):
    import frisk_sdk.telemetry.tracer_handle as module

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()
    h = module.TracerHandle.get_instance("t1", **cfg)

    provider = h._provider
    assert provider is not None

    h.acquire()
    h.acquire()
    assert h._refcount == 2

    h.release()
    assert h._refcount == 1
    assert h._closed is False
    assert cast(Any, provider).shutdown_called == 0

    h.release()
    assert h._refcount == 0
    assert h._closed is True
    assert cast(Any, provider).shutdown_called == 1

    # After full shutdown, singleton should be cleared so re-init works.
    assert module.TracerHandle._singleton is None


@pytest.mark.unit
def test_acquire_raises_after_closed(monkeypatch: pytest.MonkeyPatch):
    import frisk_sdk.telemetry.tracer_handle as module

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()
    h = module.TracerHandle.get_instance("t1", **cfg)

    # With refcount=0, release should shut down immediately.
    h.release()
    assert h._closed is True

    with pytest.raises(RuntimeError, match="closed"):
        h.acquire()


@pytest.mark.unit
def test_update_token_updates_headers_when_exporter_uses_headers_attr(
    monkeypatch: pytest.MonkeyPatch,
):
    import frisk_sdk.telemetry.tracer_handle as module

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporterHeadersProp)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()
    h = module.TracerHandle.get_instance("t1", **cfg)

    assert h._exporter is not None
    assert getattr(h._exporter, "headers") == [("authorization", "Bearer t1")]

    h.update_token("t2")
    assert getattr(h._exporter, "headers") == [("authorization", "Bearer t2")]


@pytest.mark.unit
def test_update_token_raises_when_exporter_has_no_headers_fields(
    monkeypatch: pytest.MonkeyPatch,
):
    import frisk_sdk.telemetry.tracer_handle as module

    class _ExporterWithoutHeaders:
        def __init__(self, *, endpoint: str, insecure: bool, headers):
            self.endpoint = endpoint
            self.insecure = insecure
            self._something_else = headers

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _ExporterWithoutHeaders)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()
    h = module.TracerHandle.get_instance("t1", **cfg)

    with pytest.raises(RuntimeError, match="cannot update token"):
        h.update_token("t2")


@pytest.mark.unit
def test_force_flush_true_when_closed_or_provider_missing(
    monkeypatch: pytest.MonkeyPatch,
):
    import frisk_sdk.telemetry.tracer_handle as module

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()
    h = module.TracerHandle.get_instance("t1", **cfg)

    # When closed: should be True and not raise.
    h.release()
    assert h.force_flush() is True


@pytest.mark.unit
def test_force_flush_catches_exceptions(monkeypatch: pytest.MonkeyPatch):
    import frisk_sdk.telemetry.tracer_handle as module

    class _ExplodingProvider(_FakeTracerProvider):
        def force_flush(self):
            raise RuntimeError("boom")

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _ExplodingProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()
    h = module.TracerHandle.get_instance("t1", **cfg)

    assert h.force_flush() is False


@pytest.mark.unit
def test_can_reinitialize_after_shutdown(monkeypatch: pytest.MonkeyPatch):
    import frisk_sdk.telemetry.tracer_handle as module

    monkeypatch.setattr(module, "Resource", _FakeResource)
    monkeypatch.setattr(module, "OTLPSpanExporter", _FakeOTLPSpanExporter)
    monkeypatch.setattr(module, "TracerProvider", _FakeTracerProvider)
    monkeypatch.setattr(module, "BatchSpanProcessor", _FakeBatchSpanProcessor)
    monkeypatch.setattr(module.trace, "set_tracer_provider", lambda _: None)

    cfg = _make_config()

    h1 = module.TracerHandle.get_instance("t1", **cfg).acquire()
    h1.release()
    assert module.TracerHandle._singleton is None

    h2 = module.TracerHandle.get_instance("t2", **cfg)
    assert h2 is not h1
    assert h2._exporter is not None
    assert h2._exporter._headers == [("authorization", "Bearer t2")]
