"""Chaos and cross-module integration tests for reliability and persistence wiring."""

from __future__ import annotations

import asyncio
from dataclasses import asdict
import importlib

import httpx
import pytest

from logos_adk.agent import Agent
from logos_adk.errors import CircuitBreakerOpenError
from logos_adk.observe import (
    NativeObservabilityStore,
    get_observability_store,
    reset_observability_store,
)
from logos_adk.providers.mock import MockProvider
from logos_adk.quality_history import (
    QualityHistoryStore,
    QualityRunRecord,
    reset_quality_execution_manager,
)
from logos_adk.reliability import RetryPolicy
from logos_adk.runtime import _default_runtime_holder
from logos_adk.security import InMemoryRateLimitStore


async def _request(app, method: str, path: str, **kwargs):
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
        return await client.request(method, path, **kwargs)


@pytest.fixture(autouse=True)
def _reset_test_state():
    _default_runtime_holder.reset()
    reset_observability_store().reset()
    reset_quality_execution_manager()
    yield
    _default_runtime_holder.reset()
    reset_observability_store().reset()
    reset_quality_execution_manager()


@pytest.mark.asyncio
async def test_provider_fallback_under_failure_records_audit_and_preserves_progress():
    class BrokenProvider(MockProvider):
        def __init__(self) -> None:
            super().__init__(responses=[])
            self.calls = 0

        async def generate(self, messages, tools=None, output_schema=None, **kwargs):
            self.calls += 1
            raise RuntimeError("primary unavailable")

    primary = BrokenProvider()
    fallback = MockProvider(responses=["fallback-1", "fallback-2", "fallback-3"])
    agent = (
        Agent("bot")
        .model(primary)
        .fallback(fallback)
        .reliability(
            retry_policy=RetryPolicy(max_attempts=1, base_delay=0.0, retry_on=(RuntimeError,))
        )
    )

    responses = await agent.run_batch(["one", "two", "three"], max_concurrency=3)

    assert [item.content for item in responses] == ["fallback-1", "fallback-2", "fallback-3"]
    assert primary.calls == 3
    assert len(fallback.call_history) == 3
    logs = get_observability_store()
    fallback_events = logs.query_logs(limit=20, source="security")
    assert (
        sum(
            1
            for entry in fallback_events
            if entry["attributes"].get("event") == "provider_fallback"
        )
        == 3
    )


@pytest.mark.asyncio
async def test_circuit_breaker_recovers_after_timeout_and_closes_on_success():
    class RecoveringProvider(MockProvider):
        def __init__(self) -> None:
            super().__init__(responses=[])
            self.should_fail = True

        async def generate(self, messages, tools=None, output_schema=None, **kwargs):
            if self.should_fail:
                raise RuntimeError("temporary outage")
            return await super().generate(
                messages, tools=tools, output_schema=output_schema, **kwargs
            )

    provider = RecoveringProvider()
    provider._responses = ["recovered"]
    agent = (
        Agent("bot")
        .model(provider)
        .reliability(
            retry_policy=RetryPolicy(max_attempts=1, base_delay=0.0, retry_on=(RuntimeError,))
        )
    )
    circuit = agent._provider_circuit(provider)
    circuit.failure_threshold = 1
    circuit.recovery_timeout = 0.1

    with pytest.raises(RuntimeError):
        await agent.run("first")
    assert circuit.state == "open"

    with pytest.raises(CircuitBreakerOpenError):
        await agent.run("second")

    await asyncio.sleep(0.12)
    provider.should_fail = False
    response = await agent.run("third")

    assert response.content == "recovered"
    assert circuit.state == "closed"
    assert circuit.failures == 0


@pytest.mark.asyncio
async def test_background_quality_jobs_complete_under_load():
    from logos_adk.serve import create_app

    class SlowProvider(MockProvider):
        async def generate(self, messages, tools=None, output_schema=None, **kwargs):
            await asyncio.sleep(0.02)
            return await super().generate(
                messages, tools=tools, output_schema=output_schema, **kwargs
            )

    provider = SlowProvider(responses=["ok"] * 64)
    agent = Agent("bot").model(provider)
    app = create_app([agent])

    suite = {"prompts": ["ping", "pong"], "concurrency": 2, "iterations": 3}
    submits = await asyncio.gather(
        *[
            _request(
                app,
                "POST",
                "/quality/run",
                json={"kind": "load-test", "suite": suite, "background": True},
            )
            for _ in range(6)
        ]
    )
    run_ids = []
    for response in submits:
        assert response.status_code == 202
        payload = response.json()
        assert payload["status"] == "queued"
        run_ids.append(payload["run_id"])

    async def wait_for_run(run_id: str) -> dict:
        for _ in range(100):
            detail = await _request(app, "GET", f"/quality/runs/{run_id}")
            assert detail.status_code == 200
            payload = detail.json()
            if payload["status"] == "completed":
                return payload
            await asyncio.sleep(0.02)
        raise AssertionError(f"quality run {run_id} did not complete in time")

    finished = await asyncio.gather(*(wait_for_run(run_id) for run_id in run_ids))

    assert all(item["status"] == "completed" for item in finished)
    assert all(item["summary"]["requests"] == 6 for item in finished)
    history = await _request(app, "GET", "/quality/runs?kind=load-test&limit=10")
    assert history.status_code == 200
    assert len(history.json()["items"]) >= 6


@pytest.mark.asyncio
async def test_postgresql_backed_rate_limiting_observability_and_quality_history_end_to_end(
    monkeypatch,
):
    from logos_adk.serve import create_app

    observe_module = importlib.import_module("logos_adk.observe")
    quality_history_module = importlib.import_module("logos_adk.quality_history")
    security_module = importlib.import_module("logos_adk.security")
    serve_module = importlib.import_module("logos_adk.serve")

    created_rate_limit_stores: list[FakePostgresRateLimitStore] = []

    class FakePostgresRateLimitStore(InMemoryRateLimitStore):
        def __init__(self, dsn: str, table_name: str = "rate_limit_events") -> None:
            super().__init__()
            self.dsn = dsn
            self.table_name = table_name
            self.backend = "postgresql"
            created_rate_limit_stores.append(self)

    class FakePostgresObservabilityStore(NativeObservabilityStore):
        def __init__(self) -> None:
            super().__init__()
            self.backend = "postgresql"

    class FakePostgresQualityHistoryStore(QualityHistoryStore):
        def __init__(self) -> None:
            self.backend = "postgresql"
            self.records: dict[str, QualityRunRecord] = {}

        def create_run(self, record: QualityRunRecord) -> None:
            self.records[record.id] = QualityRunRecord(**asdict(record))

        def update_run(self, run_id: str, **fields):
            current = self.records[run_id]
            payload = asdict(current)
            payload.update(fields)
            self.records[run_id] = QualityRunRecord(**payload)

        def get_run(self, run_id: str) -> QualityRunRecord | None:
            return self.records.get(run_id)

        def list_runs(
            self, *, agent_name: str | None = None, kind: str | None = None, limit: int = 50
        ):
            records = list(self.records.values())
            if agent_name is not None:
                records = [record for record in records if record.agent_name == agent_name]
            if kind is not None:
                records = [record for record in records if record.kind == kind]
            records.sort(key=lambda item: item.created_at, reverse=True)
            return records[:limit]

        def compare_runs(self, run_id_a: str, run_id_b: str) -> dict[str, object]:
            run_a = self.records[run_id_a]
            run_b = self.records[run_id_b]
            return {
                "run_a": asdict(run_a),
                "run_b": asdict(run_b),
                "summary_diff": {},
                "row_count_diff": {"a": len(run_a.rows or []), "b": len(run_b.rows or [])},
            }

    fake_observability = FakePostgresObservabilityStore()
    fake_quality_store = FakePostgresQualityHistoryStore()

    monkeypatch.setattr(security_module, "PostgreSQLRateLimitStore", FakePostgresRateLimitStore)
    monkeypatch.setattr(observe_module, "get_observability_store", lambda: fake_observability)
    monkeypatch.setattr(security_module, "get_observability_store", lambda: fake_observability)
    monkeypatch.setattr(serve_module, "get_observability_store", lambda: fake_observability)
    monkeypatch.setattr(
        quality_history_module,
        "build_quality_history_store",
        lambda config=None: fake_quality_store,
    )
    reset_quality_execution_manager()

    provider = MockProvider(responses=["ok", "ok-2"])
    agent = Agent("bot").model(provider)
    app = create_app(
        [agent],
        rate_limit_backend="postgresql",
        rate_limit_dsn="postgresql://fake/test",
        rate_limit_per_minute=1,
        rate_limit_burst=0,
    )

    allowed = await _request(app, "POST", "/run", json={"prompt": "hello"})
    limited = await _request(app, "POST", "/run", json={"prompt": "hello again"})
    quality = await _request(
        app,
        "POST",
        "/quality/run",
        json={
            "kind": "integration",
            "suite": {
                "scenarios": [
                    {"name": "pg", "steps": [{"prompt": "state this", "expect_contains": ["ok-2"]}]}
                ]
            },
        },
    )
    history = await _request(app, "GET", "/quality/runs?kind=integration")
    logs = await _request(app, "GET", "/observability/logs?limit=20")

    assert allowed.status_code == 200
    assert limited.status_code == 429
    assert quality.status_code == 200
    assert history.status_code == 200
    assert logs.status_code == 200
    assert len(created_rate_limit_stores) == 1
    assert created_rate_limit_stores[0].backend == "postgresql"
    assert fake_observability.backend == "postgresql"
    assert fake_quality_store.backend == "postgresql"
    assert len(history.json()["items"]) == 1
    assert any(item["message"] == "Rate limit exceeded" for item in logs.json()["items"])
