import asyncio
import os
import time
from typing import Any

import pytest

from fapilog.core.logger import SyncLoggerFacade
from fapilog.metrics.metrics import MetricsCollector
from fapilog.plugins.sinks.rotating_file import (
    RotatingFileSink,
    RotatingFileSinkConfig,
)

pytestmark = pytest.mark.integration


async def _monitor_loop_latency(
    stop_evt: asyncio.Event, period: float = 0.001
) -> float:
    """Monitor event-loop sleep latency; return max observed interval."""
    max_interval = 0.0
    last = time.perf_counter()
    while not stop_evt.is_set():
        await asyncio.sleep(period)
        now = time.perf_counter()
        interval = now - last
        if interval > max_interval:
            max_interval = interval
        last = now
    return max_interval


def _get_counter(registry: Any, base_name: str) -> float:
    """Fetch a Counter value from prometheus_client registry.

    Handles the `_total` sample suffix automatically (even if base already
    ends with `_total`).
    """
    for metric in registry.collect():
        if metric.name == base_name:
            for s in metric.samples:
                if s.name.endswith("_total") and not s.labels:
                    return float(s.value)
    return 0.0


def _get_gauge(registry: Any, base_name: str) -> float:
    for metric in registry.collect():
        if metric.name == base_name:
            for s in metric.samples:
                if s.name == base_name and not s.labels:
                    return float(s.value)
    return 0.0


def _get_hist_count_sum(registry: Any, base_name: str) -> tuple[int, float]:
    count = 0
    total = 0.0
    for metric in registry.collect():
        if metric.name == base_name:
            for s in metric.samples:
                if s.name == base_name + "_count":
                    count = int(s.value)
                elif s.name == base_name + "_sum":
                    total = float(s.value)
    return count, total


@pytest.mark.asyncio
async def test_load_metrics_with_drops_and_stall_bounds(tmp_path) -> None:
    # Metrics enabled to collect counters/histograms
    metrics = MetricsCollector(enabled=True)
    sink = RotatingFileSink(
        RotatingFileSinkConfig(
            directory=tmp_path,
            filename_prefix="load",
            mode="json",
            max_bytes=1_000_000,
            interval_seconds=None,
            compress_rotated=False,
        )
    )
    await sink.start()

    logger = SyncLoggerFacade(
        name="load-test",
        queue_capacity=8,  # smaller queue to ensure contention
        batch_max_size=16,  # smaller batches to create more backpressure
        batch_timeout_seconds=0.010,  # longer timeout to hold items in queue
        backpressure_wait_ms=0,  # immediate drop on full
        drop_on_full=True,  # allow drops under pressure
        sink_write=sink.write,
        metrics=metrics,
    )

    stop_evt = asyncio.Event()
    monitor_task = asyncio.create_task(_monitor_loop_latency(stop_evt))

    # Load tuned to ensure backpressure while maintaining CI stability
    total = int(os.getenv("FAPILOG_TEST_LOAD_SIZE", "4000"))

    def _produce() -> None:
        for i in range(total):
            logger.info("msg", idx=i)

    try:
        # Add timeout protection for CI environments
        await asyncio.wait_for(
            asyncio.to_thread(_produce),
            timeout=45.0,  # relaxed timeout for CI stability
        )
    except asyncio.TimeoutError as err:
        # If producer times out, that's a real bug - fail the test
        raise AssertionError(
            f"Producer timed out after 30s - submitted {logger._submitted} events"
        ) from err
    finally:
        # Always cleanup, even if producer failed
        try:
            drain = await asyncio.wait_for(
                logger.stop_and_drain(),
                timeout=10.0,  # 10 second timeout for drain
            )
        except asyncio.TimeoutError as err:
            raise AssertionError(
                "Logger drain timed out - this indicates a hang bug"
            ) from err

        await sink.stop()
        stop_evt.set()

        try:
            max_interval = await asyncio.wait_for(monitor_task, timeout=5.0)
        except asyncio.TimeoutError:
            max_interval = float("inf")  # Treat timeout as infinite stall

    # Assert loop stall within tolerance (no long blocking from sink/rotation)
    # Allow override via env in CI; enforce a minimum bound of 0.10s to reduce flakiness on slow runners
    stall_bound = max(
        float(os.getenv("FAPILOG_TEST_MAX_LOOP_STALL_SECONDS", "0.20")), 0.10
    )
    assert max_interval < stall_bound

    # Metrics assertions
    reg = metrics.registry
    assert reg is not None
    dropped = _get_counter(reg, "fapilog_events_dropped_total")
    flush_count, flush_sum = _get_hist_count_sum(reg, "fapilog_flush_seconds")
    q_hwm = _get_gauge(reg, "fapilog_queue_high_watermark")

    # Validate basic metrics and processing
    # Either drops occurred (backpressure tested) OR all events processed (high performance)
    # Both scenarios are valid - the key is that the system behaves correctly
    assert drain.submitted == total  # All events were submitted
    accounted = drain.processed + drain.dropped
    # Allow a tiny accounting delta under high concurrency to reduce flakiness.
    assert abs(accounted - total) <= 1

    # If drops occurred, validate backpressure metrics are working
    if (dropped > 0) or (drain.dropped > 0):
        # Backpressure behavior was exercised - good for testing
        pass
    else:
        # High performance path - all events processed without drops
        assert drain.processed == total

    assert flush_count > 0
    assert 0 < q_hwm <= logger._queue.capacity

    # Average flush latency should be sane; allow override via env
    avg_flush = (flush_sum / flush_count) if flush_count else 0.0
    # Allow CI override; enforce a minimum bound to reduce flakiness on slow runners
    flush_bound = max(
        float(os.getenv("FAPILOG_TEST_MAX_AVG_FLUSH_SECONDS", "0.30")), 1.00
    )
    assert avg_flush < flush_bound


@pytest.mark.asyncio
async def test_load_metrics_no_drops_and_low_latency(tmp_path) -> None:
    metrics = MetricsCollector(enabled=True)
    sink = RotatingFileSink(
        RotatingFileSinkConfig(
            directory=tmp_path,
            filename_prefix="load",
            mode="json",
            max_bytes=5_000_000,
            interval_seconds=None,
            compress_rotated=False,
        )
    )
    await sink.start()

    logger = SyncLoggerFacade(
        name="load-test",
        queue_capacity=8_192,  # ample capacity to avoid drops
        batch_max_size=64,
        batch_timeout_seconds=0.010,
        backpressure_wait_ms=5,
        drop_on_full=False,
        sink_write=sink.write,
        metrics=metrics,
    )

    stop_evt = asyncio.Event()
    monitor_task = asyncio.create_task(_monitor_loop_latency(stop_evt))

    total = int(os.getenv("FAPILOG_TEST_LOAD_SIZE_NO_DROPS", "3500"))

    def _produce() -> None:
        for i in range(total):
            logger.info("ok", n=i)

    try:
        # Add timeout protection for CI environments
        await asyncio.wait_for(
            asyncio.to_thread(_produce),
            timeout=float(os.getenv("FAPILOG_TEST_PRODUCER_TIMEOUT", "45.0")),
        )
    except asyncio.TimeoutError as err:
        raise AssertionError(
            f"Producer timed out after 20s - submitted {logger._submitted} events"
        ) from err
    finally:
        # Always cleanup with timeouts
        try:
            drain = await asyncio.wait_for(logger.stop_and_drain(), timeout=10.0)
        except asyncio.TimeoutError as err:
            raise AssertionError(
                "Logger drain timed out - this indicates a hang bug"
            ) from err

        await sink.stop()
        stop_evt.set()

        try:
            max_interval = await asyncio.wait_for(monitor_task, timeout=5.0)
        except asyncio.TimeoutError:
            max_interval = float("inf")

    # No drops expected
    assert drain.dropped == 0
    # Floor raised to 0.25 to accommodate CI environments under load
    stall_bound = max(
        float(os.getenv("FAPILOG_TEST_MAX_LOOP_STALL_SECONDS", "0.25")), 0.25
    )
    assert max_interval < stall_bound

    reg = metrics.registry
    assert reg
    dropped = _get_counter(reg, "fapilog_events_dropped_total")
    flush_count, flush_sum = _get_hist_count_sum(reg, "fapilog_flush_seconds")

    assert dropped == 0
    assert flush_count > 0
    avg_flush = (flush_sum / flush_count) if flush_count else 0.0
    flush_bound = max(
        float(os.getenv("FAPILOG_TEST_MAX_AVG_FLUSH_SECONDS", "0.30")), 1.00
    )
    assert avg_flush < flush_bound
