"""Тесты `GET /metrics`.

Правила общей dev-БД: проверяем факт наличия метрики/дельту,
а не абсолютные значения — dev-инстанс уже содержит "живые" сеансы и
запросы, накопленные другими тестами/ручным использованием. Метрика
глубины очереди Redis проверяется на throwaway-ключе (не на реальных
`transcription`/`summarize`/`notify`/`celery`) — запись фиктивной "задачи" в
настоящую очередь рисковала бы её подхватом реальным Celery-воркером
dev-стенда.
"""

import uuid
from collections.abc import Iterable
from datetime import UTC, datetime

import httpx
import pytest
from prometheus_client.parser import text_string_to_metric_families
from prometheus_client.samples import Sample
from sqlalchemy.ext.asyncio import AsyncSession

from api import metrics as metrics_module
from core.config import get_settings
from core.redis import redis_client
from models.conference import Conference
from models.session import ConferenceSession
from services.conference_ids import generate_number, generate_slug


def _samples(body: str, family_name: str) -> list[Sample]:
    """Все сэмплы семейства метрик `family_name` из тела ответа `/metrics`."""
    result: list[Sample] = []
    for family in text_string_to_metric_families(body):
        if family.name == family_name:
            result.extend(family.samples)
    return result


def _sample_value(samples: Iterable[Sample], *, suffix: str, **labels: str) -> float | None:
    """Значение сэмпла (`name` заканчивается на `suffix`) с точным совпадением меток."""
    for sample in samples:
        if sample.name.endswith(suffix) and all(
            sample.labels.get(k) == v for k, v in labels.items()
        ):
            return sample.value
    return None


async def test_metrics_endpoint_returns_prometheus_exposition_format(
    client: httpx.AsyncClient,
) -> None:
    """`/metrics` без авторизации отдаёт текстовый формат Prometheus с нашими метриками."""
    response = await client.get("/metrics")
    assert response.status_code == 200
    assert response.headers["content-type"].startswith("text/plain")

    families = {family.name for family in text_string_to_metric_families(response.text)}
    assert "vidconf_http_request_duration_seconds" in families
    assert "vidconf_pipeline_sessions" in families
    assert "vidconf_celery_queue_depth" in families
    assert "vidconf_host_info" in families
    assert "vidconf_db_up" in families
    assert "vidconf_db_pool_size" in families
    assert "vidconf_db_pool_max_overflow" in families
    assert "vidconf_db_pool_checked_out" in families
    assert "vidconf_redis_pool_in_use" in families
    assert "vidconf_redis_pool_max_connections" in families


async def test_metrics_host_info_gauge_reflects_settings(
    client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """`vidconf_host_info` отдаёт `HW_*` из настроек как лейблы, значение всегда 1."""
    settings = get_settings()
    monkeypatch.setattr(settings, "hw_cpus", 4)
    monkeypatch.setattr(settings, "hw_ram_mb", 7937)
    monkeypatch.setattr(settings, "hw_gpu_name", None)
    monkeypatch.setattr(settings, "hw_vram_mb", None)

    response = await client.get("/metrics")

    samples = _samples(response.text, "vidconf_host_info")
    value = _sample_value(
        samples,
        suffix="vidconf_host_info",
        cpus="4",
        ram_mb="7937",
        gpu_name="—",
        vram_mb="—",
    )
    assert value == 1


async def test_metrics_records_http_latency_by_route(client: httpx.AsyncClient) -> None:
    """Латентность известного маршрута (`GET /api/health`) попадает в histogram с этим `path`."""
    await client.get("/api/health")
    response = await client.get("/metrics")

    samples = _samples(response.text, "vidconf_http_request_duration_seconds")
    count = _sample_value(samples, suffix="_count", method="GET", path="/api/health")
    assert count is not None and count >= 1


async def test_metrics_pipeline_sessions_gauge_reflects_new_session(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Добавление сеанса в статусе `failed` увеличивает gauge для этого статуса ровно на 1."""
    before_response = await client.get("/metrics")
    before = _sample_value(
        _samples(before_response.text, "vidconf_pipeline_sessions"),
        suffix="vidconf_pipeline_sessions",
        status="failed",
    )
    assert before is not None

    conference = Conference(number=generate_number(), slug=generate_slug(), title="Metrics Test")
    db_session.add(conference)
    await db_session.flush()
    db_session.add(
        ConferenceSession(
            conference_id=conference.id,
            title="Metrics Test",
            t_start=datetime.now(UTC),
            pipeline_status="failed",
        )
    )
    await db_session.flush()

    after_response = await client.get("/metrics")
    after = _sample_value(
        _samples(after_response.text, "vidconf_pipeline_sessions"),
        suffix="vidconf_pipeline_sessions",
        status="failed",
    )
    assert after == before + 1


async def test_metrics_db_up_gauge_reflects_real_connectivity(client: httpx.AsyncClient) -> None:
    """Против реального тестового Postgres (см. докстринг conftest) `vidconf_db_up` == 1."""
    response = await client.get("/metrics")
    value = _sample_value(_samples(response.text, "vidconf_db_up"), suffix="vidconf_db_up")
    assert value == 1


async def test_metrics_db_up_gauge_reports_down_without_crashing_endpoint(
    client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Недоступность БД (проверка вне пула не удалась) не роняет `/metrics` — отдаёт 0, не 500."""

    async def _fail() -> bool:
        return False

    monkeypatch.setattr(metrics_module, "check_db_up", _fail)

    response = await client.get("/metrics")

    assert response.status_code == 200
    value = _sample_value(_samples(response.text, "vidconf_db_up"), suffix="vidconf_db_up")
    assert value == 0


async def test_metrics_db_pool_gauges_reflect_settings_not_usage(
    client: httpx.AsyncClient,
) -> None:
    """`vidconf_db_pool_size`/`_max_overflow` — конфигурация из `Settings`, не текущая занятость."""
    settings = get_settings()
    response = await client.get("/metrics")

    samples_size = _samples(response.text, "vidconf_db_pool_size")
    samples_overflow = _samples(response.text, "vidconf_db_pool_max_overflow")
    size = _sample_value(samples_size, suffix="vidconf_db_pool_size")
    max_overflow = _sample_value(samples_overflow, suffix="vidconf_db_pool_max_overflow")

    assert size == settings.db_pool_size
    assert max_overflow == settings.db_max_overflow


async def test_metrics_endpoint_survives_pipeline_gauge_failure(
    client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Падение/таймаут основного пула на одном gauge не роняет весь `/metrics`.

    Симулирует ровно ситуацию инцидента 07.08 (`api/metrics.py` падал вместе
    со всем остальным при исчерпанном пуле): `count_by_pipeline_status`
    поднимает исключение — `vidconf_db_up`/`vidconf_db_pool_*` (не зависящие
    от основного пула) при этом всё равно приходят в ответе.
    """

    async def _raise(*args: object, **kwargs: object) -> dict[str, int]:
        raise TimeoutError("основной пул занят (симуляция теста)")

    monkeypatch.setattr(
        "repositories.conferences.ConferenceSessionRepository.count_by_pipeline_status",
        _raise,
    )

    response = await client.get("/metrics")

    assert response.status_code == 200
    db_up = _sample_value(_samples(response.text, "vidconf_db_up"), suffix="vidconf_db_up")
    assert db_up == 1


async def test_metrics_celery_queue_depth_gauge(
    client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Gauge глубины очереди отражает реальный `LLEN` отслеживаемого списка Redis."""
    throwaway_queue = f"test-queue-{uuid.uuid4()}"
    monkeypatch.setattr(metrics_module, "CELERY_QUEUES", (throwaway_queue,))
    await redis_client.rpush(throwaway_queue, "a", "b", "c")
    try:
        response = await client.get("/metrics")
    finally:
        await redis_client.delete(throwaway_queue)

    value = _sample_value(
        _samples(response.text, "vidconf_celery_queue_depth"),
        suffix="vidconf_celery_queue_depth",
        queue=throwaway_queue,
    )
    assert value == 3
