"""Tests for rbsp.runtime.monitoring -- Timer, Metrics, SlowQueryLog, PerformanceReport."""

from __future__ import annotations

import os
import sys
import time
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.runtime.monitoring import (
    Metrics,
    PerformanceReport,
    SlowQueryLog,
    Timer,
)


# ---------------------------------------------------------------------------
# Timer
# ---------------------------------------------------------------------------

class TestTimerBasic(unittest.TestCase):
    """Timer records wall-clock elapsed time."""

    def test_initial_elapsed_is_zero(self):
        t = Timer("test")
        self.assertEqual(t.elapsed, 0.0)
        self.assertEqual(t.elapsed_ms, 0.0)

    def test_name_stored(self):
        t = Timer("my_timer")
        self.assertEqual(t.name, "my_timer")

    def test_default_name(self):
        t = Timer()
        self.assertEqual(t.name, "")


class TestTimerContextManager(unittest.TestCase):
    """Timer as a context manager."""

    def test_elapsed_positive_after_context(self):
        with Timer("test") as t:
            time.sleep(0.01)
        self.assertGreater(t.elapsed, 0.0)
        self.assertGreater(t.elapsed_ms, 0.0)

    def test_elapsed_ms_is_seconds_times_1000(self):
        with Timer("test") as t:
            time.sleep(0.01)
        self.assertAlmostEqual(t.elapsed_ms, t.elapsed * 1000.0, places=5)

    def test_elapsed_during_context_is_live(self):
        with Timer("test") as t:
            time.sleep(0.01)
            mid = t.elapsed
            self.assertGreater(mid, 0.0)
        # After context, elapsed should be >= mid
        self.assertGreaterEqual(t.elapsed, mid)

    def test_reentrant_resets(self):
        t = Timer("test")
        with t:
            time.sleep(0.01)
        first = t.elapsed
        with t:
            time.sleep(0.01)
        second = t.elapsed
        # Both should be positive; second is independently timed
        self.assertGreater(first, 0.0)
        self.assertGreater(second, 0.0)


# ---------------------------------------------------------------------------
# Metrics -- counters
# ---------------------------------------------------------------------------

class TestMetricsCounters(unittest.TestCase):
    """Metrics counter operations."""

    def setUp(self):
        self.m = Metrics()

    def test_increment_default(self):
        self.m.counter("queries")
        self.assertEqual(self.m.get_counter("queries"), 1)

    def test_increment_by_value(self):
        self.m.counter("queries", 5)
        self.assertEqual(self.m.get_counter("queries"), 5)

    def test_accumulates(self):
        self.m.counter("queries", 3)
        self.m.counter("queries", 7)
        self.assertEqual(self.m.get_counter("queries"), 10)

    def test_unset_counter_is_zero(self):
        self.assertEqual(self.m.get_counter("nonexistent"), 0)


# ---------------------------------------------------------------------------
# Metrics -- gauges
# ---------------------------------------------------------------------------

class TestMetricsGauges(unittest.TestCase):
    """Metrics gauge operations."""

    def setUp(self):
        self.m = Metrics()

    def test_set_and_get(self):
        self.m.gauge("memory", 1024.5)
        self.assertEqual(self.m.get_gauge("memory"), 1024.5)

    def test_overwrite(self):
        self.m.gauge("memory", 100.0)
        self.m.gauge("memory", 200.0)
        self.assertEqual(self.m.get_gauge("memory"), 200.0)

    def test_unset_gauge_is_zero(self):
        self.assertEqual(self.m.get_gauge("nonexistent"), 0.0)


# ---------------------------------------------------------------------------
# Metrics -- histograms
# ---------------------------------------------------------------------------

class TestMetricsHistograms(unittest.TestCase):
    """Metrics histogram operations."""

    def setUp(self):
        self.m = Metrics()

    def test_record_and_snapshot(self):
        for v in [10.0, 20.0, 30.0]:
            self.m.histogram("latency", v)
        h = self.m.get_histogram("latency")
        self.assertEqual(h["count"], 3)
        self.assertEqual(h["min"], 10.0)
        self.assertEqual(h["max"], 30.0)
        self.assertAlmostEqual(h["avg"], 20.0, places=5)

    def test_unset_histogram_returns_zeros(self):
        h = self.m.get_histogram("nonexistent")
        self.assertEqual(h["count"], 0)
        self.assertEqual(h["min"], 0.0)

    def test_percentiles_single_value(self):
        self.m.histogram("single", 42.0)
        h = self.m.get_histogram("single")
        self.assertEqual(h["p50"], 42.0)
        self.assertEqual(h["p95"], 42.0)
        self.assertEqual(h["p99"], 42.0)

    def test_percentiles_multiple_values(self):
        for v in range(1, 101):
            self.m.histogram("range", float(v))
        h = self.m.get_histogram("range")
        # p50 should be around 50
        self.assertGreater(h["p50"], 40.0)
        self.assertLess(h["p50"], 60.0)
        # p99 should be around 99
        self.assertGreater(h["p99"], 90.0)


# ---------------------------------------------------------------------------
# Metrics -- timer shortcut
# ---------------------------------------------------------------------------

class TestMetricsTimer(unittest.TestCase):
    """Metrics.timer() auto-records to histogram."""

    def test_timer_records_to_histogram(self):
        m = Metrics()
        with m.timer("query_time"):
            time.sleep(0.01)
        h = m.get_histogram("query_time")
        self.assertEqual(h["count"], 1)
        self.assertGreater(h["max"], 0.0)


# ---------------------------------------------------------------------------
# Metrics -- snapshot and reset
# ---------------------------------------------------------------------------

class TestMetricsSnapshot(unittest.TestCase):
    """Metrics.snapshot() returns all data."""

    def test_snapshot_structure(self):
        m = Metrics()
        m.counter("q", 1)
        m.gauge("mem", 512.0)
        m.histogram("lat", 10.0)
        snap = m.snapshot()
        self.assertIn("counters", snap)
        self.assertIn("gauges", snap)
        self.assertIn("histograms", snap)
        self.assertEqual(snap["counters"]["q"], 1)
        self.assertEqual(snap["gauges"]["mem"], 512.0)
        self.assertEqual(snap["histograms"]["lat"]["count"], 1)

    def test_snapshot_empty(self):
        m = Metrics()
        snap = m.snapshot()
        self.assertEqual(snap["counters"], {})
        self.assertEqual(snap["gauges"], {})
        self.assertEqual(snap["histograms"], {})


class TestMetricsReset(unittest.TestCase):
    """Metrics.reset() clears everything."""

    def test_reset_clears_all(self):
        m = Metrics()
        m.counter("q", 5)
        m.gauge("mem", 100.0)
        m.histogram("lat", 50.0)
        m.reset()
        self.assertEqual(m.get_counter("q"), 0)
        self.assertEqual(m.get_gauge("mem"), 0.0)
        snap = m.snapshot()
        self.assertEqual(snap["histograms"], {})


# ---------------------------------------------------------------------------
# Metrics -- singleton
# ---------------------------------------------------------------------------

class TestMetricsSingleton(unittest.TestCase):
    """Metrics.get_instance() returns a singleton."""

    def setUp(self):
        Metrics.reset_instance()

    def tearDown(self):
        Metrics.reset_instance()

    def test_get_instance_returns_same_object(self):
        a = Metrics.get_instance()
        b = Metrics.get_instance()
        self.assertIs(a, b)

    def test_reset_instance_creates_new(self):
        a = Metrics.get_instance()
        Metrics.reset_instance()
        b = Metrics.get_instance()
        self.assertIsNot(a, b)


# ---------------------------------------------------------------------------
# SlowQueryLog
# ---------------------------------------------------------------------------

class TestSlowQueryLog(unittest.TestCase):
    """SlowQueryLog records queries exceeding threshold."""

    def test_logs_slow_query(self):
        log = SlowQueryLog(threshold_ms=50.0)
        log.log("select *", 100.0, 5)
        entries = log.get_slow_queries()
        self.assertEqual(len(entries), 1)
        self.assertEqual(entries[0]["query"], "select *")
        self.assertEqual(entries[0]["duration_ms"], 100.0)
        self.assertEqual(entries[0]["results_count"], 5)

    def test_ignores_fast_query(self):
        log = SlowQueryLog(threshold_ms=50.0)
        log.log("select *", 10.0, 5)
        entries = log.get_slow_queries()
        self.assertEqual(len(entries), 0)

    def test_threshold_boundary(self):
        log = SlowQueryLog(threshold_ms=50.0)
        log.log("exact", 50.0, 1)  # exactly at threshold
        entries = log.get_slow_queries()
        self.assertEqual(len(entries), 1)

    def test_below_threshold(self):
        log = SlowQueryLog(threshold_ms=50.0)
        log.log("fast", 49.99, 1)
        entries = log.get_slow_queries()
        self.assertEqual(len(entries), 0)

    def test_max_entries_eviction(self):
        log = SlowQueryLog(threshold_ms=0.0, max_entries=3)
        for i in range(5):
            log.log(f"query_{i}", float(i + 1), i)
        entries = log.get_slow_queries()
        self.assertEqual(len(entries), 3)
        # Should keep the last 3
        self.assertEqual(entries[0]["query"], "query_2")
        self.assertEqual(entries[2]["query"], "query_4")

    def test_clear(self):
        log = SlowQueryLog(threshold_ms=0.0)
        log.log("q1", 10.0, 1)
        log.log("q2", 20.0, 2)
        log.clear()
        self.assertEqual(log.get_slow_queries(), [])

    def test_timestamp_present(self):
        log = SlowQueryLog(threshold_ms=0.0)
        log.log("q", 10.0, 1)
        entry = log.get_slow_queries()[0]
        self.assertIn("timestamp", entry)
        self.assertIsInstance(entry["timestamp"], float)

    def test_get_returns_copy(self):
        log = SlowQueryLog(threshold_ms=0.0)
        log.log("q", 10.0, 1)
        entries1 = log.get_slow_queries()
        entries2 = log.get_slow_queries()
        self.assertIsNot(entries1, entries2)


# ---------------------------------------------------------------------------
# PerformanceReport
# ---------------------------------------------------------------------------

class TestPerformanceReport(unittest.TestCase):
    """PerformanceReport.generate() produces a human-readable report."""

    def test_empty_metrics(self):
        m = Metrics()
        report = PerformanceReport.generate(m)
        self.assertIn("RBSP Performance Report", report)
        self.assertIn("No metrics collected", report)
        self.assertIn("End Report", report)

    def test_with_counters(self):
        m = Metrics()
        m.counter("queries", 42)
        report = PerformanceReport.generate(m)
        self.assertIn("Counters:", report)
        self.assertIn("queries: 42", report)

    def test_with_gauges(self):
        m = Metrics()
        m.gauge("memory_mb", 512.0)
        report = PerformanceReport.generate(m)
        self.assertIn("Gauges:", report)
        self.assertIn("memory_mb:", report)

    def test_with_histograms(self):
        m = Metrics()
        m.histogram("latency", 10.0)
        m.histogram("latency", 20.0)
        report = PerformanceReport.generate(m)
        self.assertIn("Histograms:", report)
        self.assertIn("latency:", report)
        self.assertIn("count:", report)
        self.assertIn("p50:", report)
        self.assertIn("p99:", report)

    def test_all_sections(self):
        m = Metrics()
        m.counter("q", 1)
        m.gauge("mem", 256.0)
        m.histogram("lat", 5.0)
        report = PerformanceReport.generate(m)
        self.assertIn("Counters:", report)
        self.assertIn("Gauges:", report)
        self.assertIn("Histograms:", report)
        self.assertIn("=== RBSP Performance Report ===", report)
        self.assertIn("=== End Report ===", report)


if __name__ == "__main__":
    unittest.main()
