"""Tests for api/metrics.py - in-memory metrics with histograms, gauges, Prometheus export."""

import threading

from api.metrics import (
    _MAX_HISTOGRAM_SAMPLES,
    _MAX_METRICS,
    gauge,
    histogram,
    inc,
    log_summary,
    observe,
    render_prometheus,
    reset,
    snapshot,
    timer,
)


class TestCounters:
    def setup_method(self):
        reset()

    def test_inc_basic(self):
        inc("requests")
        snap = snapshot()
        assert snap["requests"] == 1

    def test_inc_custom_value(self):
        inc("bytes", 1024)
        snap = snapshot()
        assert snap["bytes"] == 1024

    def test_inc_accumulates(self):
        inc("x")
        inc("x")
        inc("x", 3)
        assert snapshot()["x"] == 5

    def test_inc_max_metrics_cap(self):
        # Fill to capacity
        for i in range(_MAX_METRICS):
            inc(f"metric_{i}")
        # New metric should be silently dropped
        inc("overflow_metric")
        snap = snapshot()
        assert "overflow_metric" not in snap
        assert len([k for k in snap if k.startswith("metric_")]) == _MAX_METRICS


class TestObservations:
    def setup_method(self):
        reset()

    def test_observe_basic(self):
        observe("latency", 10.0)
        observe("latency", 20.0)
        observe("latency", 30.0)
        snap = snapshot()
        assert snap["latency.count"] == 3
        assert snap["latency.sum"] == 60.0
        assert snap["latency.max"] == 30.0
        assert snap["latency.avg"] == 20.0

    def test_observe_rejects_nan(self):
        observe("x", float("nan"))
        snap = snapshot()
        assert "x.count" not in snap

    def test_observe_rejects_inf(self):
        observe("x", float("inf"))
        snap = snapshot()
        assert "x.count" not in snap

    def test_observe_rejects_non_numeric(self):
        observe("x", "not a number")  # type: ignore
        snap = snapshot()
        assert "x.count" not in snap


class TestGauges:
    def setup_method(self):
        reset()

    def test_gauge_set(self):
        gauge("cpu", 75.5)
        assert snapshot()["cpu"] == 75.5

    def test_gauge_overwrites(self):
        gauge("cpu", 50.0)
        gauge("cpu", 90.0)
        assert snapshot()["cpu"] == 90.0

    def test_gauge_rejects_nan(self):
        gauge("x", float("nan"))
        assert "x" not in snapshot()


class TestHistograms:
    def setup_method(self):
        reset()

    def test_histogram_percentiles(self):
        # Insert 100 values: 1..100
        for i in range(1, 101):
            histogram("dur", float(i))
        snap = snapshot()
        assert snap["dur.count"] == 100
        assert snap["dur.p50"] == 51.0  # index 50
        assert snap["dur.p95"] == 96.0  # index 95
        assert snap["dur.p99"] == 100.0  # index 99
        assert snap["dur.max"] == 100.0

    def test_histogram_small_sample(self):
        # With < 20 samples, p95 falls back to max
        for i in range(5):
            histogram("small", float(i))
        snap = snapshot()
        assert snap["small.p95"] == 4.0  # max
        assert snap["small.p50"] == 2.0  # index 2 (5//2) -> sorted[2] = 2.0

    def test_histogram_bounded_buffer(self):
        # Insert more than max samples
        for i in range(_MAX_HISTOGRAM_SAMPLES + 500):
            histogram("big", float(i))
        snap = snapshot()
        assert snap["big.count"] == _MAX_HISTOGRAM_SAMPLES

    def test_histogram_rejects_nan(self):
        histogram("x", float("nan"))
        assert "x.count" not in snapshot()


class TestTimer:
    def setup_method(self):
        reset()

    def test_timer_records(self):
        with timer("op"):
            pass  # nearly instant
        snap = snapshot()
        assert "op.count" in snap
        assert snap["op.count"] >= 1


class TestSnapshot:
    def setup_method(self):
        reset()

    def test_snapshot_all_types(self):
        inc("counter", 5)
        observe("obs", 10.0)
        gauge("g", 42.0)
        histogram("hist", 7.0)
        snap = snapshot()
        assert snap["counter"] == 5
        assert snap["obs.count"] == 1
        assert snap["g"] == 42.0
        assert snap["hist.p50"] == 7.0


class TestReset:
    def test_reset_clears_all(self):
        inc("a")
        observe("b", 1.0)
        gauge("c", 2.0)
        histogram("d", 3.0)
        reset()
        snap = snapshot()
        assert snap == {}


class TestPrometheus:
    def setup_method(self):
        reset()

    def test_render_prometheus_format(self):
        inc("http.requests.total", 42)
        gauge("cpu.usage", 75.5)
        output = render_prometheus()
        assert "hikaflow_http_requests_total 42" in output
        assert "hikaflow_cpu_usage 75.5" in output

    def test_render_prometheus_empty(self):
        output = render_prometheus()
        assert output == ""

    def test_render_prometheus_replaces_dots_and_dashes(self):
        inc("my-metric.count", 1)
        output = render_prometheus()
        assert "hikaflow_my_metric_count 1" in output


class TestLogSummary:
    def setup_method(self):
        reset()

    def test_log_summary_no_error_on_empty(self):
        log_summary()  # should not raise

    def test_log_summary_no_error_with_data(self):
        inc("x", 5)
        log_summary()  # should not raise


class TestThreadSafety:
    def setup_method(self):
        reset()

    def test_concurrent_inc(self):
        errors = []

        def worker():
            try:
                for _ in range(1000):
                    inc("concurrent")
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=worker) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors
        assert snapshot()["concurrent"] == 10000
