"""Unit tests for handler/metrics.py"""

import time
import threading
from unittest.mock import patch, MagicMock

_mock_log = MagicMock()
_mock_log.get_logger = MagicMock(return_value=MagicMock())
with patch("gmi_ieops.handler.metrics.log", _mock_log):
    from gmi_ieops.handler.metrics import Metrics, get_metrics, reset_global_metrics
    import gmi_ieops.handler.metrics as _mod
_mod.log = _mock_log


class TestMetrics:

    def test_counter(self):
        m = Metrics()
        m.inc("c"); m.inc("c"); m.inc("c", 3)
        assert m.get_counter("c") == 5.0
        assert m.get_counter("missing") == 0.0

    def test_counter_with_labels(self):
        m = Metrics()
        m.inc("req", labels={"method": "GET"})
        m.inc("req", labels={"method": "GET"})
        m.inc("req", labels={"method": "POST"})
        assert m.get_counter("req", labels={"method": "GET"}) == 2.0
        assert m.get_counter("req", labels={"method": "POST"}) == 1.0

    def test_gauge(self):
        m = Metrics()
        m.set_gauge("g", 10)
        assert m.get_gauge("g") == 10.0
        m.inc_gauge("g", 3)
        assert m.get_gauge("g") == 13.0
        m.dec_gauge("g", 5)
        assert m.get_gauge("g") == 8.0
        assert m.get_gauge("missing") == 0.0

    def test_gauge_with_labels(self):
        m = Metrics()
        m.set_gauge("gpu", 80, labels={"id": "0"})
        m.set_gauge("gpu", 60, labels={"id": "1"})
        assert m.get_gauge("gpu", labels={"id": "0"}) == 80.0

    def test_histogram_and_summary(self):
        m = Metrics()
        m.register_histogram("lat", buckets=(0.1, 0.5, 1.0))
        m.observe("lat", 0.3)
        m.observe("sum_metric", 1024)  # auto-creates summary
        text = "\n".join(m.export(include_endpoint=False))
        assert "lat_bucket" in text and "lat_count" in text
        assert "sum_metric_count" in text

    def test_timer(self):
        m = Metrics()
        with m.timer("dur"):
            time.sleep(0.01)
        assert "dur_count" in "\n".join(m.export(include_endpoint=False))

    def test_track_request(self):
        m = Metrics()
        m.track_request(success=True, latency=0.1, prompt_tokens=100, completion_tokens=50)
        assert m.get_counter("requests_total") == 1.0
        assert m.get_counter("requests_success_total") == 1.0
        assert m.get_counter("prompt_tokens_total") == 100.0

        m2 = Metrics()
        m2.track_request(success=False)
        assert m2.get_counter("requests_failed_total") == 1.0

    def test_inc_negative_ignored(self):
        """Counter.inc with negative value should be silently ignored, not crash."""
        m = Metrics()
        m.inc("safe_counter", 5)
        m.inc("safe_counter", -3)  # should be ignored
        m.inc("safe_counter", 0)   # should be ignored
        assert m.get_counter("safe_counter") == 5.0

    def test_track_request_negative_tokens(self):
        """Negative token values should not crash track_request."""
        m = Metrics()
        m.track_request(prompt_tokens=-100, completion_tokens=-50, latency=-1.0)
        assert m.get_counter("requests_total") == 1.0
        assert m.get_counter("prompt_tokens_total") == 0.0  # not recorded

    def test_export_and_reset(self):
        m = Metrics()
        m.inc("x")
        assert any("x" in l for l in m.export(include_endpoint=False))
        m.reset()
        assert "x" not in "\n".join(m.export(include_endpoint=False))

    def test_thread_safety(self):
        m = Metrics()
        errors = []
        def worker(i):
            try:
                for j in range(100):
                    m.inc(f"c_{i % 3}"); m.set_gauge(f"g_{i % 3}", j)
            except Exception as e:
                errors.append(e)
        threads = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
        for t in threads: t.start()
        for t in threads: t.join()
        assert not errors


class TestGlobalMetrics:

    def setup_method(self):
        _mod._metrics = None

    def test_singleton(self):
        assert get_metrics() is get_metrics()

    def test_reset(self):
        m = get_metrics()
        m.inc("gc")
        reset_global_metrics()
        assert "gc" not in "\n".join(m.export(include_endpoint=False))
