"""Stable production tests for semver, observability, reliability, and security."""
from __future__ import annotations

from datetime import UTC, datetime

import pytest

from logos_adk.agent import Agent
from logos_adk import API_VERSION, PUBLIC_API_MODULES, STABILITY_POLICY
from logos_adk.observe import InMemoryExporter, NativeObservabilityStore, Span, get_observability_store
from logos_adk.providers.mock import MockProvider
from logos_adk.scaling import SQLiteTaskQueue
from logos_adk.security import AuditLogger, DeletionRequest
from logos_adk.types import Response, Usage
from logos_adk.version import __version__


@pytest.mark.asyncio
async def test_agent_spans_include_version_and_config_metadata():
    exporter = InMemoryExporter()
    agent = Agent("bot", model=MockProvider(responses=["ok"])).observe(exporter)

    result = await agent.run("hello", session_id="s1")

    assert result.content == "ok"
    root_span = next(span for span in exporter.spans if span.name == "Agent.run")
    assert root_span.attributes["framework.version"] == __version__
    assert root_span.attributes["framework.api_version"] == API_VERSION
    assert root_span.attributes["agent.config.path"] == "inline"
    assert len(root_span.attributes["agent.config.fingerprint"]) == 16


@pytest.mark.asyncio
async def test_agent_error_span_carries_exception_type():
    class BrokenProvider(MockProvider):
        async def generate(self, messages, tools=None, output_schema=None, **kwargs):
            raise RuntimeError("boom")

    exporter = InMemoryExporter()
    agent = Agent("bot", model=BrokenProvider()).observe(exporter)

    with pytest.raises(RuntimeError, match="boom"):
        await agent.run("hello", session_id="s1")

    root_span = [span for span in exporter.spans if span.name == "Agent.run"][-1]
    assert root_span.status == "error"
    assert root_span.attributes["exception_type"] == "RuntimeError"


@pytest.mark.asyncio
async def test_sqlite_task_queue_supports_idempotency_and_dead_letter_replay(tmp_path):
    queue = SQLiteTaskQueue(path=str(tmp_path / "tasks.db"))
    calls = 0

    async def work() -> Response:
        nonlocal calls
        calls += 1
        return Response(
            content=f"result-{calls}",
            tool_calls=[],
            usage=Usage(input_tokens=1, output_tokens=1),
            raw={},
        )

    first = await queue.submit("bot", work, idempotency_key="same-key")
    second = await queue.submit("bot", work, idempotency_key="same-key")

    assert first.content == "result-1"
    assert second.content == "result-1"
    assert calls == 1

    failures = 0

    async def always_fail() -> Response:
        nonlocal failures
        failures += 1
        raise RuntimeError("fail")

    with pytest.raises(RuntimeError, match="fail"):
        await queue.submit("bot", always_fail, max_retries=1)

    stats = queue.stats()
    assert stats["failed"] == 1
    assert stats["dead_letter"] == 1
    assert failures == 2

    replay = await queue.replay(
        "dead-letter-task",
        "bot",
        work,
        max_retries=0,
    )
    assert replay.content == "result-2"


def test_observability_dashboard_and_summary_cover_new_failure_modes():
    store = NativeObservabilityStore()
    now = datetime.now(UTC)
    for exc_type in ("ProviderTimeoutError", "CircuitBreakerOpenError", "StateBackendError"):
        store.record_span(
            Span(
                name="Agent.run",
                trace_id=f"trace-{exc_type}",
                span_id=f"span-{exc_type}",
                parent_span_id=None,
                started_at=now,
                ended_at=now,
                duration_ms=10.0,
                agent_name="bot",
                session_id="s1",
                status="error",
                attributes={"exception_type": exc_type},
            )
        )

    summary = store.summary()
    assert summary["totals"]["provider_timeouts_total"] == 1.0
    assert summary["totals"]["circuit_breaker_open_total"] == 1.0
    assert summary["totals"]["state_backend_failures_total"] == 1.0

    targets = {panel["target"] for panel in store.grafana_dashboard()["panels"]}
    assert "logos_adk_provider_timeouts_total" in targets
    assert "logos_adk_circuit_breaker_open_total" in targets
    assert "logos_adk_state_backend_failures_total" in targets


def test_audit_logger_redacts_pii_and_records_deletion_requests():
    store = get_observability_store()
    store.reset()
    logger = AuditLogger()
    logger.log(
        event="pii_detection",
        outcome="warning",
        attributes={"findings": [{"pii_type": "email", "match": "secret@example.com"}]},
    )
    logger.record_deletion_request(
        DeletionRequest(
            subject_id="customer-1",
            scope="workspace:acme",
            requested_at="2026-03-18T00:00:00+00:00",
        )
    )

    logs = store.latest_logs(limit=2)
    assert logs[1]["attributes"]["findings"][0]["match"] == "[REDACTED]"
    assert logs[0]["attributes"]["event"] == "deletion_request"


def test_stable_api_surface_is_explicit():
    assert STABILITY_POLICY == "semver"
    assert "logos_adk" in PUBLIC_API_MODULES
