"""
Tests for AutoPIL OpenTelemetry integration.

Fixtures wire directly into autopil.telemetry module-level vars
instead of using OTEL global state (which is a singleton that can
only be set once per process).
"""

import pytest
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter

import autopil.telemetry as telemetry


# ── fixtures ───────────────────────────────────────────────────────────────────

@pytest.fixture(autouse=True)
def reset_telemetry():
    telemetry.reset()
    yield
    telemetry.reset()


@pytest.fixture
def span_exporter():
    """InMemorySpanExporter wired directly into telemetry._tracer (no global state)."""
    exporter = InMemorySpanExporter()
    provider = TracerProvider()
    provider.add_span_processor(SimpleSpanProcessor(exporter))
    # Bypass the OTEL global TracerProvider singleton — assign directly.
    telemetry._tracer = provider.get_tracer("autopil-test")
    return exporter


@pytest.fixture
def metric_reader():
    """InMemoryMetricReader wired directly into telemetry metric instruments."""
    reader = InMemoryMetricReader()
    provider = MeterProvider(metric_readers=[reader])
    meter = provider.get_meter("autopil-test")
    telemetry._eval_counter   = meter.create_counter("autopil.evaluate.count")
    telemetry._deny_counter   = meter.create_counter("autopil.evaluate.denied.count")
    telemetry._duration_histo = meter.create_histogram("autopil.evaluate.duration", unit="ms")
    return reader


# ── helpers ────────────────────────────────────────────────────────────────────

def _counter_value(reader, metric_name, **filter_attrs):
    data = reader.get_metrics_data()
    if data is None:
        return 0
    for rm in data.resource_metrics:
        for sm in rm.scope_metrics:
            for m in sm.metrics:
                if m.name != metric_name:
                    continue
                for dp in m.data.data_points:
                    if all(dp.attributes.get(k) == v for k, v in filter_attrs.items()):
                        return dp.value
    return 0


# ── no-op path ─────────────────────────────────────────────────────────────────

def test_setup_noop_without_env(monkeypatch):
    monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
    monkeypatch.delenv("OTEL_TRACES_EXPORTER", raising=False)
    monkeypatch.delenv("OTEL_METRICS_EXPORTER", raising=False)
    assert telemetry.setup() is False
    assert telemetry._tracer is None
    assert telemetry._eval_counter is None


def test_noop_span_set_result_does_not_raise():
    span = telemetry._NoOpSpan()
    span.set_result(decision="DENY", policy_name="p", reason="blocked")


def test_evaluate_span_yields_noop_when_not_configured():
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t1", session_id="s1",
    ) as span:
        assert isinstance(span, telemetry._NoOpSpan)
        span.set_result(decision="ALLOW", policy_name="analyst_policy")


def test_evaluate_span_safe_without_set_result():
    """No crash if set_result is never called."""
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t1", session_id="s1",
    ):
        pass


# ── span attributes ────────────────────────────────────────────────────────────

def test_evaluate_span_allow_sets_attributes(span_exporter):
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="market_data",
        tenant_id="ten_abc", session_id="sess_123",
        sensitivity_level="medium",
    ) as span:
        span.set_result(decision="ALLOW", policy_name="analyst_policy", reason="Access granted")

    spans = span_exporter.get_finished_spans()
    assert len(spans) == 1
    attrs = spans[0].attributes
    assert spans[0].name                      == "autopil.evaluate"
    assert attrs["autopil.agent_role"]        == "analyst"
    assert attrs["autopil.source_id"]         == "market_data"
    assert attrs["autopil.tenant_id"]         == "ten_abc"
    assert attrs["autopil.session_id"]        == "sess_123"
    assert attrs["autopil.sensitivity_level"] == "medium"
    assert attrs["autopil.decision"]          == "ALLOW"
    assert attrs["autopil.policy_name"]       == "analyst_policy"
    assert attrs["autopil.latency_ms"]        >= 0.0


def test_evaluate_span_deny_marks_error(span_exporter):
    from opentelemetry.trace import StatusCode

    with telemetry.evaluate_span(
        agent_role="viewer", source_id="executive_comms",
        tenant_id="t", session_id="s",
    ) as span:
        span.set_result(decision="DENY", policy_name="viewer_policy", reason="Source is denied")

    s = span_exporter.get_finished_spans()[0]
    assert s.attributes["autopil.decision"] == "DENY"
    assert s.attributes["autopil.reason"]   == "Source is denied"
    assert s.status.status_code             == StatusCode.ERROR


def test_evaluate_span_allow_not_marked_error(span_exporter):
    from opentelemetry.trace import StatusCode

    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t", session_id="s",
    ) as span:
        span.set_result(decision="ALLOW", policy_name="p")

    s = span_exporter.get_finished_spans()[0]
    assert s.status.status_code != StatusCode.ERROR


def test_span_omits_sensitivity_when_not_provided(span_exporter):
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t", session_id="s",
    ) as span:
        span.set_result(decision="ALLOW", policy_name="p")

    s = span_exporter.get_finished_spans()[0]
    assert "autopil.sensitivity_level" not in s.attributes


def test_multiple_spans_independent(span_exporter):
    for i in range(3):
        with telemetry.evaluate_span(
            agent_role="analyst", source_id=f"src_{i}",
            tenant_id="t", session_id=f"s{i}",
        ) as span:
            span.set_result(decision="ALLOW", policy_name="p")

    spans = span_exporter.get_finished_spans()
    assert len(spans) == 3
    sources = {s.attributes["autopil.source_id"] for s in spans}
    assert sources == {"src_0", "src_1", "src_2"}


# ── metrics ────────────────────────────────────────────────────────────────────

def test_eval_counter_incremented_on_allow(metric_reader):
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t1", session_id="s1",
    ) as span:
        span.set_result(decision="ALLOW", policy_name="analyst_policy")

    assert _counter_value(
        metric_reader, "autopil.evaluate.count",
        **{"autopil.decision": "ALLOW", "autopil.tenant_id": "t1"}
    ) == 1


def test_deny_counter_incremented_on_deny(metric_reader):
    with telemetry.evaluate_span(
        agent_role="viewer", source_id="executive_comms",
        tenant_id="t1", session_id="s1",
    ) as span:
        span.set_result(decision="DENY", policy_name="viewer_policy")

    assert _counter_value(
        metric_reader, "autopil.evaluate.denied.count",
        **{"autopil.decision": "DENY"}
    ) == 1


def test_deny_counter_zero_on_allow(metric_reader):
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t1", session_id="s1",
    ) as span:
        span.set_result(decision="ALLOW", policy_name="p")

    assert _counter_value(metric_reader, "autopil.evaluate.denied.count") == 0


def test_duration_histogram_records_value(metric_reader):
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t1", session_id="s1",
    ) as span:
        span.set_result(decision="ALLOW", policy_name="p")

    data = metric_reader.get_metrics_data()
    found = False
    for rm in data.resource_metrics:
        for sm in rm.scope_metrics:
            for m in sm.metrics:
                if m.name == "autopil.evaluate.duration":
                    for dp in m.data.data_points:
                        assert dp.sum >= 0
                        found = True
    assert found, "duration histogram metric not found"


def test_no_metrics_without_set_result(metric_reader):
    with telemetry.evaluate_span(
        agent_role="analyst", source_id="reports",
        tenant_id="t1", session_id="s1",
    ):
        pass  # never call set_result

    assert _counter_value(metric_reader, "autopil.evaluate.count") == 0


def test_eval_counter_accumulates(metric_reader):
    for _ in range(5):
        with telemetry.evaluate_span(
            agent_role="analyst", source_id="reports",
            tenant_id="t1", session_id="s",
        ) as span:
            span.set_result(decision="ALLOW", policy_name="p")

    assert _counter_value(
        metric_reader, "autopil.evaluate.count",
        **{"autopil.tenant_id": "t1"}
    ) == 5


# ── console exporter setup ────────────────────────────────────────────────────

def test_setup_console_traces_returns_true(monkeypatch):
    monkeypatch.setenv("OTEL_TRACES_EXPORTER", "console")
    monkeypatch.delenv("OTEL_METRICS_EXPORTER", raising=False)
    assert telemetry.setup() is True
    assert telemetry._tracer is not None


# ── guard integration ─────────────────────────────────────────────────────────

def test_guard_emits_span_on_allow(span_exporter, policy_file, audit_db):
    from autopil.guard import ContextGuard
    from autopil.models import SensitivityLevel

    guard = ContextGuard(policy_path=policy_file, audit_db=audit_db)

    @guard.protect(agent_role="analyst", user_id="u1", source_id="reports",
                   sensitivity_level=SensitivityLevel.LOW)
    def retrieve(query):
        return ["doc1"]

    result = retrieve("what is the trend?")
    assert result == ["doc1"]

    spans = span_exporter.get_finished_spans()
    assert len(spans) == 1
    assert spans[0].attributes["autopil.decision"]  == "ALLOW"
    assert spans[0].attributes["autopil.source_id"] == "reports"
    assert spans[0].attributes["autopil.agent_role"] == "analyst"


def test_guard_emits_span_on_deny(span_exporter, policy_file, audit_db):
    from autopil.guard import ContextGuard
    from autopil.models import SensitivityLevel

    guard = ContextGuard(policy_path=policy_file, audit_db=audit_db)

    @guard.protect(agent_role="analyst", user_id="u1", source_id="executive_comms",
                   sensitivity_level=SensitivityLevel.HIGH)
    def retrieve(query):
        return []

    with pytest.raises(PermissionError):
        retrieve("show me the board deck")

    spans = span_exporter.get_finished_spans()
    assert len(spans) == 1
    assert spans[0].attributes["autopil.decision"]  == "DENY"
    assert spans[0].attributes["autopil.source_id"] == "executive_comms"


# ── REST API integration ───────────────────────────────────────────────────────

def test_api_evaluate_allow_emits_span(span_exporter, client):
    client.post("/v1/context/evaluate", json={
        "query": "trend analysis",
        "agent_role": "analyst",
        "user_id": "u1",
        "source_id": "reports",
        "sensitivity_level": "low",
    })
    spans = span_exporter.get_finished_spans()
    assert len(spans) == 1
    s = spans[0]
    assert s.name                       == "autopil.evaluate"
    assert s.attributes["autopil.agent_role"] == "analyst"
    assert s.attributes["autopil.decision"]   == "ALLOW"
    assert s.attributes["autopil.tenant_id"]  != ""


def test_api_evaluate_deny_span_marked_error(span_exporter, client):
    from opentelemetry.trace import StatusCode

    client.post("/v1/context/evaluate", json={
        "query": "show board deck",
        "agent_role": "analyst",
        "user_id": "u1",
        "source_id": "executive_comms",
        "sensitivity_level": "high",
    })
    spans = span_exporter.get_finished_spans()
    assert len(spans) == 1
    s = spans[0]
    assert s.attributes["autopil.decision"] == "DENY"
    assert s.status.status_code             == StatusCode.ERROR
