"""Tests for the Prometheus metrics system."""

import threading
import time
import pytest
from unittest.mock import MagicMock

from horizon.metrics import (
    MetricsCollector,
    MetricsServer,
    update_engine_metrics,
)


class TestCounter:
    def test_initial_value(self):
        c = MetricsCollector()
        assert c.counter("test").value == 0.0

    def test_increment(self):
        c = MetricsCollector()
        c.counter("orders").inc()
        assert c.counter("orders").value == 1.0

    def test_increment_amount(self):
        c = MetricsCollector()
        c.counter("fills").inc(5.0)
        assert c.counter("fills").value == 5.0

    def test_multiple_increments(self):
        c = MetricsCollector()
        c.counter("events").inc()
        c.counter("events").inc(3.0)
        assert c.counter("events").value == 4.0

    def test_same_counter_returned(self):
        c = MetricsCollector()
        c1 = c.counter("x")
        c2 = c.counter("x")
        c1.inc()
        assert c2.value == 1.0


class TestGauge:
    def test_initial_value(self):
        c = MetricsCollector()
        assert c.gauge("test").value == 0.0

    def test_set(self):
        c = MetricsCollector()
        c.gauge("open_orders").set(5.0)
        assert c.gauge("open_orders").value == 5.0

    def test_overwrite(self):
        c = MetricsCollector()
        c.gauge("pnl").set(100.0)
        c.gauge("pnl").set(-50.0)
        assert c.gauge("pnl").value == -50.0

    def test_gauge_inc(self):
        c = MetricsCollector()
        c.gauge("count").set(10.0)
        c.gauge("count").inc(2.0)
        assert c.gauge("count").value == 12.0


class TestHistogram:
    def test_observe(self):
        c = MetricsCollector()
        h = c.histogram("latency")
        h.observe(0.005)
        h.observe(0.01)
        h.observe(0.1)
        rendered = h.render("latency")
        assert "latency_count" in rendered
        assert "latency_sum" in rendered
        assert "latency_bucket" in rendered

    def test_count_and_sum(self):
        c = MetricsCollector()
        h = c.histogram("test_hist")
        h.observe(1.0)
        h.observe(2.0)
        h.observe(3.0)
        rendered = h.render("test_hist")
        assert 'test_hist_count 3' in rendered
        assert 'test_hist_sum 6.0' in rendered


class TestRender:
    def test_empty_render(self):
        c = MetricsCollector()
        output = c.render()
        assert output == "\n"

    def test_render_counter(self):
        c = MetricsCollector()
        c.counter("horizon_orders").inc(10)
        output = c.render()
        assert "# TYPE horizon_orders counter" in output
        assert "horizon_orders 10" in output

    def test_render_gauge(self):
        c = MetricsCollector()
        c.gauge("horizon_pnl").set(42.5)
        output = c.render()
        assert "# TYPE horizon_pnl gauge" in output
        assert "horizon_pnl 42.5" in output

    def test_render_histogram(self):
        c = MetricsCollector()
        c.histogram("horizon_latency").observe(0.01)
        output = c.render()
        assert "# TYPE horizon_latency histogram" in output
        assert "horizon_latency_bucket" in output

    def test_render_all_types(self):
        c = MetricsCollector()
        c.counter("c1").inc()
        c.gauge("g1").set(5)
        c.histogram("h1").observe(0.1)
        output = c.render()
        assert "# TYPE c1 counter" in output
        assert "# TYPE g1 gauge" in output
        assert "# TYPE h1 histogram" in output


class TestMetricsServer:
    def test_start_stop(self):
        c = MetricsCollector()
        server = MetricsServer(c, port=0)  # port 0 = OS assigns
        # Just verify it doesn't crash; port=0 lets OS assign
        # We test the server lifecycle only
        # Note: port=0 in HTTPServer picks a random available port

    def test_server_with_real_port(self):
        import socket
        # Find a free port
        sock = socket.socket()
        sock.bind(("127.0.0.1", 0))
        port = sock.getsockname()[1]
        sock.close()

        c = MetricsCollector()
        c.counter("test_counter").inc(42)
        c.gauge("test_gauge").set(3.14)

        server = MetricsServer(c, port=port)
        server.start()
        try:
            time.sleep(0.1)
            import urllib.request
            resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/metrics")
            body = resp.read().decode()
            assert "test_counter 42" in body
            assert "test_gauge 3.14" in body
        finally:
            server.stop()

    def test_server_404_for_other_paths(self):
        import socket
        import urllib.request
        import urllib.error

        sock = socket.socket()
        sock.bind(("127.0.0.1", 0))
        port = sock.getsockname()[1]
        sock.close()

        c = MetricsCollector()
        server = MetricsServer(c, port=port)
        server.start()
        try:
            time.sleep(0.1)
            with pytest.raises(urllib.error.HTTPError) as exc_info:
                urllib.request.urlopen(f"http://127.0.0.1:{port}/other")
            assert exc_info.value.code == 404
        finally:
            server.stop()


class TestUpdateEngineMetrics:
    def test_update_from_engine(self):
        engine = MagicMock()
        status = MagicMock()
        status.open_orders = 5
        status.active_positions = 2
        status.total_realized_pnl = 100.0
        status.total_unrealized_pnl = -20.0
        status.daily_pnl = 80.0
        status.kill_switch_active = False
        status.uptime_secs = 3600
        engine.status.return_value = status

        c = MetricsCollector()
        update_engine_metrics(c, engine)

        assert c.gauge("horizon_open_orders").value == 5
        assert c.gauge("horizon_active_positions").value == 2
        assert c.gauge("horizon_total_realized_pnl").value == 100.0
        assert c.gauge("horizon_total_unrealized_pnl").value == -20.0
        assert c.gauge("horizon_daily_pnl").value == 80.0
        assert c.gauge("horizon_kill_switch").value == 0.0
        assert c.gauge("horizon_uptime_secs").value == 3600

    def test_update_kill_switch_active(self):
        engine = MagicMock()
        status = MagicMock()
        status.open_orders = 0
        status.active_positions = 0
        status.total_realized_pnl = 0.0
        status.total_unrealized_pnl = 0.0
        status.daily_pnl = 0.0
        status.kill_switch_active = True
        status.uptime_secs = 0
        engine.status.return_value = status

        c = MetricsCollector()
        update_engine_metrics(c, engine)
        assert c.gauge("horizon_kill_switch").value == 1.0


class TestThreadSafety:
    def test_concurrent_counter_increments(self):
        c = MetricsCollector()
        counter = c.counter("concurrent")
        n_threads = 10
        n_increments = 1000

        def worker():
            for _ in range(n_increments):
                counter.inc()

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

        assert counter.value == n_threads * n_increments

    def test_concurrent_gauge_sets(self):
        c = MetricsCollector()
        gauge = c.gauge("concurrent_gauge")
        n_threads = 10

        def worker(val):
            for _ in range(100):
                gauge.set(val)

        threads = [threading.Thread(target=worker, args=(i,)) for i in range(n_threads)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # Final value should be one of the thread values (0-9)
        assert 0 <= gauge.value < n_threads
