"""Tests for the metrics module."""

from __future__ import annotations

import re
import threading
import time

import pytest

from oclawma.metrics import MetricsCollector, MetricsServer, format_prometheus_metrics
from oclawma.metrics.collector import HistogramTimer
from oclawma.queue import JobQueue


class TestMetricsCollector:
    """Test metrics collector functionality."""

    @pytest.fixture
    def collector(self):
        """Create a fresh metrics collector for each test."""
        return MetricsCollector()

    def test_initial_state(self, collector):
        """Test initial state of metrics collector."""
        assert collector.jobs_processed == 0
        assert collector.jobs_failed == 0
        assert collector.jobs_completed == 0
        assert collector.queue_depth == 0
        assert collector.jobs_by_status == {}

    def test_increment_processed(self, collector):
        """Test incrementing processed counter."""
        collector.increment_processed()
        assert collector.jobs_processed == 1

        collector.increment_processed(5)
        assert collector.jobs_processed == 6

    def test_increment_failed(self, collector):
        """Test incrementing failed counter."""
        collector.increment_failed()
        assert collector.jobs_failed == 1

        collector.increment_failed(3)
        assert collector.jobs_failed == 4

    def test_increment_completed(self, collector):
        """Test incrementing completed counter."""
        collector.increment_completed()
        assert collector.jobs_completed == 1

        collector.increment_completed(2)
        assert collector.jobs_completed == 3

    def test_set_queue_depth(self, collector):
        """Test setting queue depth gauge."""
        collector.set_queue_depth(10)
        assert collector.queue_depth == 10

        collector.set_queue_depth(0)
        assert collector.queue_depth == 0

    def test_record_duration(self, collector):
        """Test recording job duration."""
        collector.record_duration(1.5)
        collector.record_duration(2.5)
        collector.record_duration(0.5)

        assert collector.duration_count == 3
        assert collector.duration_sum == 4.5
        assert collector.duration_buckets[0.5] == 1
        assert collector.duration_buckets[1.0] == 1
        assert collector.duration_buckets[2.5] == 3
        assert collector.duration_buckets[5.0] == 3
        assert collector.duration_buckets[float("inf")] == 3

    def test_record_duration_with_histogram_timer(self, collector):
        """Test recording duration using HistogramTimer context manager."""
        with HistogramTimer(collector, "job_duration"):
            time.sleep(0.01)  # Small sleep to ensure non-zero duration

        assert collector.duration_count >= 1
        assert collector.duration_sum > 0

    def test_update_job_status_count(self, collector):
        """Test updating job status counts."""
        collector.update_job_status_count("pending", 5)
        collector.update_job_status_count("running", 2)
        collector.update_job_status_count("completed", 10)

        assert collector.jobs_by_status["pending"] == 5
        assert collector.jobs_by_status["running"] == 2
        assert collector.jobs_by_status["completed"] == 10

    def test_get_metrics_snapshot(self, collector):
        """Test getting metrics snapshot."""
        collector.increment_processed(10)
        collector.increment_failed(2)
        collector.increment_completed(8)
        collector.set_queue_depth(5)
        collector.record_duration(1.0)
        collector.update_job_status_count("pending", 5)

        snapshot = collector.get_metrics_snapshot()

        assert snapshot["jobs_processed"] == 10
        assert snapshot["jobs_failed"] == 2
        assert snapshot["jobs_completed"] == 8
        assert snapshot["queue_depth"] == 5
        assert snapshot["jobs_by_status"]["pending"] == 5
        assert snapshot["duration_count"] == 1
        assert snapshot["duration_sum"] == 1.0
        assert "timestamp" in snapshot

    def test_thread_safety(self, collector):
        """Test thread safety of metrics collector."""
        errors = []

        def increment_task():
            try:
                for _ in range(100):
                    collector.increment_processed()
                    collector.increment_completed()
                    collector.record_duration(0.1)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=increment_task) for _ in range(10)]

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert collector.jobs_processed == 1000
        assert collector.jobs_completed == 1000
        assert collector.duration_count == 1000


class TestPrometheusFormat:
    """Test Prometheus text format output."""

    @pytest.fixture
    def collector(self):
        """Create a metrics collector with sample data."""
        c = MetricsCollector()
        c.increment_processed(100)
        c.increment_failed(5)
        c.increment_completed(95)
        c.set_queue_depth(10)
        c.record_duration(0.5)
        c.record_duration(1.5)
        c.record_duration(2.5)
        c.update_job_status_count("pending", 10)
        c.update_job_status_count("running", 2)
        c.update_job_status_count("completed", 95)
        c.update_job_status_count("failed", 5)
        return c

    def test_prometheus_format_output(self, collector):
        """Test Prometheus format contains expected metrics."""
        output = format_prometheus_metrics(collector)

        # Check for required metric names
        assert "# HELP oclawma_jobs_processed_total" in output
        assert "# TYPE oclawma_jobs_processed_total counter" in output
        assert "oclawma_jobs_processed_total 100" in output

        assert "# HELP oclawma_jobs_failed_total" in output
        assert "# TYPE oclawma_jobs_failed_total counter" in output
        assert "oclawma_jobs_failed_total 5" in output

        assert "# HELP oclawma_jobs_completed_total" in output
        assert "# TYPE oclawma_jobs_completed_total counter" in output
        assert "oclawma_jobs_completed_total 95" in output

        assert "# HELP oclawma_queue_depth" in output
        assert "# TYPE oclawma_queue_depth gauge" in output
        assert "oclawma_queue_depth 10" in output

        assert "# HELP oclawma_job_duration_seconds" in output
        assert "# TYPE oclawma_job_duration_seconds histogram" in output

        assert "# HELP oclawma_jobs_by_status" in output
        assert "# TYPE oclawma_jobs_by_status gauge" in output

    def test_prometheus_duration_histogram(self, collector):
        """Test duration histogram buckets in Prometheus format."""
        output = format_prometheus_metrics(collector)

        # Check histogram buckets
        assert 'oclawma_job_duration_seconds_bucket{le="0.005"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="0.01"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="0.025"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="0.05"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="0.1"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="0.25"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="0.5"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="1.0"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="2.5"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="5.0"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="10.0"}' in output
        assert 'oclawma_job_duration_seconds_bucket{le="+Inf"}' in output

        # Check _sum and _count
        assert "oclawma_job_duration_seconds_sum 4.5" in output
        assert "oclawma_job_duration_seconds_count 3" in output

    def test_prometheus_status_gauge_labels(self, collector):
        """Test status gauge with labels."""
        output = format_prometheus_metrics(collector)

        # Check status gauges with labels
        assert 'oclawma_jobs_by_status{status="pending"} 10' in output
        assert 'oclawma_jobs_by_status{status="running"} 2' in output
        assert 'oclawma_jobs_by_status{status="completed"} 95' in output
        assert 'oclawma_jobs_by_status{status="failed"} 5' in output


class TestMetricsJSONFormat:
    """Test JSON format output."""

    @pytest.fixture
    def collector(self):
        """Create a metrics collector with sample data."""
        c = MetricsCollector()
        c.increment_processed(50)
        c.increment_failed(2)
        c.increment_completed(48)
        c.set_queue_depth(5)
        c.record_duration(1.0)
        c.update_job_status_count("pending", 5)
        c.update_job_status_count("completed", 48)
        return c

    def test_json_format(self, collector):
        """Test JSON format output."""
        snapshot = collector.get_metrics_snapshot()

        assert snapshot["jobs_processed"] == 50
        assert snapshot["jobs_failed"] == 2
        assert snapshot["jobs_completed"] == 48
        assert snapshot["queue_depth"] == 5
        assert snapshot["duration_count"] == 1
        assert snapshot["duration_sum"] == 1.0
        assert snapshot["jobs_by_status"]["pending"] == 5
        assert snapshot["jobs_by_status"]["completed"] == 48
        assert "duration_buckets" in snapshot
        assert "timestamp" in snapshot


class TestQueueIntegration:
    """Test integration with JobQueue."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_metrics.db"
        with JobQueue(db_path) as q:
            yield q

    @pytest.fixture
    def collector(self):
        """Create a fresh metrics collector."""
        return MetricsCollector()

    def test_collect_from_queue(self, collector, queue):
        """Test collecting metrics from queue."""
        # Add jobs
        queue.enqueue({"task": "a"})
        queue.enqueue({"task": "b"})
        _ = queue.enqueue({"task": "c"})

        # Process one job - dequeue removes it from pending (changes to RUNNING)
        dequeued_job = queue.dequeue()
        assert dequeued_job is not None
        queue.complete(dequeued_job.id)

        # Collect metrics
        collector.collect_from_queue(queue)

        # Should have 2 pending (1 dequeued+completed, 2 still pending)
        assert collector.queue_depth == 2  # 2 pending
        assert collector.jobs_by_status["pending"] == 2
        assert collector.jobs_by_status["completed"] == 1

    def test_track_job_lifecycle(self, collector, queue):
        """Test tracking full job lifecycle."""
        # Create tracked wrapper
        from oclawma.metrics import TrackedJobQueue

        tracked = TrackedJobQueue(queue, collector)

        # Enqueue and process a job
        _ = tracked.enqueue({"task": "test"})

        # Should update queue depth
        assert collector.queue_depth >= 1

        # Dequeue and complete
        dequeued = tracked.dequeue()
        assert dequeued is not None

        tracked.complete(dequeued.id)

        # Should have recorded metrics
        assert collector.jobs_processed >= 1
        assert collector.jobs_completed >= 1

    def test_track_job_failure(self, collector, queue):
        """Test tracking job failure."""
        from oclawma.metrics import TrackedJobQueue

        tracked = TrackedJobQueue(queue, collector)

        _ = tracked.enqueue({"task": "fail"})
        dequeued = tracked.dequeue()
        assert dequeued is not None

        tracked.fail(dequeued.id, "Test error")

        assert collector.jobs_failed >= 1


class TestMetricsServer:
    """Test metrics HTTP server."""

    @pytest.fixture
    def collector(self):
        """Create a metrics collector."""
        c = MetricsCollector()
        c.increment_processed(10)
        c.set_queue_depth(5)
        return c

    @pytest.fixture
    def server(self, collector):
        """Create a metrics server on a random port."""
        return MetricsServer(collector, host="127.0.0.1", port=0)

    def test_server_creation(self, server):
        """Test server creation."""
        assert server.host == "127.0.0.1"
        assert server.collector is not None

    def test_server_start_stop(self, server):
        """Test starting and stopping server."""
        try:
            server.start()
            assert server.is_running
            assert server.port is not None
            assert server.port > 0
        finally:
            server.stop()
            assert not server.is_running

    def test_prometheus_endpoint(self, server):
        """Test Prometheus metrics endpoint."""
        import httpx

        try:
            server.start()
            port = server.port

            response = httpx.get(f"http://127.0.0.1:{port}/metrics")

            assert response.status_code == 200
            assert "oclawma_jobs_processed_total" in response.text
            assert "oclawma_queue_depth" in response.text
            assert "text/plain" in response.headers["content-type"]
        finally:
            server.stop()

    def test_json_endpoint(self, server):
        """Test JSON metrics endpoint."""
        import httpx

        try:
            server.start()
            port = server.port

            response = httpx.get(f"http://127.0.0.1:{port}/metrics/json")

            assert response.status_code == 200
            data = response.json()
            assert data["jobs_processed"] == 10
            assert data["queue_depth"] == 5
            assert "timestamp" in data
        finally:
            server.stop()

    def test_health_endpoint(self, server):
        """Test health check endpoint."""
        import httpx

        try:
            server.start()
            port = server.port

            response = httpx.get(f"http://127.0.0.1:{port}/health")

            assert response.status_code == 200
            assert response.json()["status"] == "healthy"
        finally:
            server.stop()

    def test_404_endpoint(self, server):
        """Test 404 for unknown endpoints."""
        import httpx

        try:
            server.start()
            port = server.port

            response = httpx.get(f"http://127.0.0.1:{port}/unknown")

            assert response.status_code == 404
        finally:
            server.stop()


class TestHistogramTimer:
    """Test HistogramTimer context manager."""

    @pytest.fixture
    def collector(self):
        """Create a metrics collector."""
        return MetricsCollector()

    def test_timer_records_duration(self, collector):
        """Test timer records duration."""
        with HistogramTimer(collector, "job_duration"):
            time.sleep(0.02)

        assert collector.duration_count == 1
        assert collector.duration_sum >= 0.02

    def test_timer_with_exception(self, collector):
        """Test timer still records even if exception occurs."""
        with pytest.raises(ValueError), HistogramTimer(collector, "job_duration"):
            time.sleep(0.01)
            raise ValueError("Test error")

        assert collector.duration_count == 1

    def test_timer_manual_observation(self, collector):
        """Test manual observation after timer."""
        timer = HistogramTimer(collector, "job_duration")
        timer.start()
        time.sleep(0.01)
        timer.observe()

        assert collector.duration_count == 1


class TestMetricsRegistry:
    """Test metrics registry for global collector."""

    def test_get_global_collector(self):
        """Test getting global collector."""
        from oclawma.metrics import get_collector, reset_collector

        reset_collector()
        c1 = get_collector()
        c2 = get_collector()

        assert c1 is c2

        # Increment on one should reflect on the other
        c1.increment_processed()
        assert c2.jobs_processed == 1

    def test_reset_collector(self):
        """Test resetting global collector."""
        from oclawma.metrics import get_collector, reset_collector

        c = get_collector()
        c.increment_processed(10)

        reset_collector()
        c2 = get_collector()

        assert c2.jobs_processed == 0


class TestPrometheusOutputValidation:
    """Validate Prometheus output format."""

    @pytest.fixture
    def collector(self):
        """Create collector with data."""
        c = MetricsCollector()
        c.increment_processed(100)
        c.increment_failed(5)
        c.increment_completed(95)
        c.set_queue_depth(10)
        c.record_duration(0.5)
        c.update_job_status_count("pending", 10)
        return c

    def test_prometheus_format_valid(self, collector):
        """Test that output follows Prometheus exposition format."""
        output = format_prometheus_metrics(collector)

        # Check for valid format patterns
        lines = output.strip().split("\n")

        # Check each non-empty line
        for line in lines:
            line = line.strip()
            if not line:
                continue

            if line.startswith("#"):
                # Comment line - should be HELP or TYPE
                assert line.startswith("# HELP") or line.startswith(
                    "# TYPE"
                ), f"Invalid comment: {line}"
            else:
                # Metric line should match: name{labels} value [timestamp]
                if "{" in line:
                    match = re.match(r"^(\w+)\{([^}]*)\}\s+([\d.eE+-]+)(?:\s+\d+)?$", line)
                else:
                    match = re.match(r"^(\w+)\s+([\d.eE+-]+)(?:\s+\d+)?$", line)
                assert match, f"Invalid metric line format: {line}"

    def test_prometheus_counter_values_always_increase(self, collector):
        """Test that counters only increase."""
        output = format_prometheus_metrics(collector)

        # Parse counter values
        counters = {}
        for line in output.split("\n"):
            if line.startswith("oclawma_jobs_") and "_total" in line and not line.startswith("#"):
                parts = line.split()
                if len(parts) >= 2:
                    name = parts[0].split("{")[0]
                    value = float(parts[1])
                    counters[name] = value

        # All counters should be >= 0
        for name, value in counters.items():
            assert value >= 0, f"Counter {name} has negative value: {value}"

    def test_prometheus_histogram_buckets_monotonic(self, collector):
        """Test that histogram buckets are monotonically increasing."""
        output = format_prometheus_metrics(collector)

        # Extract bucket values
        buckets = []
        for line in output.split("\n"):
            if 'oclawma_job_duration_seconds_bucket{le="' in line:
                # Extract bucket boundary and count
                match = re.search(r'le="([^"]+)"\}\s+(\d+)', line)
                if match:
                    boundary = match.group(1)
                    count = int(match.group(2))
                    buckets.append((boundary, count))

        # Counts should be monotonically increasing
        prev_count = 0
        for boundary, count in buckets:
            assert count >= prev_count, f"Bucket {boundary} has decreasing count"
            prev_count = count
