"""Tests for daemon heartbeat + error ring buffer + devices endpoint."""

import json
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from threading import Thread
from unittest.mock import patch

import pytest

from qc_trace.daemon.config import DaemonConfig
from qc_trace.daemon.pusher import Pusher


class TestSendHeartbeat:
    """Test Pusher.send_heartbeat()."""

    @pytest.fixture
    def server(self):
        received = []

        class Handler(BaseHTTPRequestHandler):
            def do_POST(self):
                length = int(self.headers.get("Content-Length", 0))
                body = self.rfile.read(length)
                received.append(json.loads(body))
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.end_headers()
                self.wfile.write(b'{"ok":true}')

            def log_message(self, format, *args):
                pass

        srv = HTTPServer(("127.0.0.1", 0), Handler)
        port = srv.server_address[1]
        t = Thread(target=srv.serve_forever, daemon=True)
        t.start()
        yield port, received
        srv.shutdown()

    def test_send_heartbeat_success(self, server):
        port, received = server
        config = DaemonConfig(ingest_url=f"http://127.0.0.1:{port}/ingest")
        pusher = Pusher(config=config)

        payload = {
            "device_id": "test-device-123",
            "device_name": "test-host",
            "org": "test-org",
            "daemon_version": "0.4.17",
            "status": "pushing",
            "queue_size": 0,
            "current_backoff": 0,
            "messages_this_session": 42,
            "recent_errors": [],
        }
        result = pusher.send_heartbeat(payload)
        assert isinstance(result, dict)
        assert result["ok"] is True
        assert len(received) == 1
        assert received[0]["device_id"] == "test-device-123"
        assert received[0]["status"] == "pushing"

    def test_send_heartbeat_failure(self):
        config = DaemonConfig(ingest_url="http://127.0.0.1:19999/ingest")
        pusher = Pusher(config=config)

        result = pusher.send_heartbeat({"device_id": "x", "status": "idle"})
        assert result is None

    def test_send_heartbeat_with_errors(self, server):
        port, received = server
        config = DaemonConfig(ingest_url=f"http://127.0.0.1:{port}/ingest")
        pusher = Pusher(config=config)

        payload = {
            "device_id": "test-device-456",
            "status": "degraded",
            "recent_errors": [
                {"ts": "2026-02-10T16:28:47Z", "error": "HTTP 503", "count": 63}
            ],
        }
        result = pusher.send_heartbeat(payload)
        assert isinstance(result, dict)
        assert result["ok"] is True
        assert received[0]["recent_errors"][0]["error"] == "HTTP 503"


class TestErrorRingBuffer:
    """Test push_status error recording with tracebacks."""

    def test_record_and_read_errors(self, tmp_path):
        """Errors are stored individually (not deduped) with tracebacks."""
        status_file = tmp_path / "push_status.json"
        lock_file = tmp_path / "push_status.lock"

        with patch("qc_trace.daemon.push_status.PUSH_STATUS_FILE", status_file), \
             patch("qc_trace.daemon.push_status._LOCK_FILE", lock_file):
            from qc_trace.daemon.push_status import record_error, get_recent_errors

            record_error("HTTP 503: Service Unavailable", traceback_text="Traceback:\n  File foo.py\nHTTPError: 503")
            record_error("HTTP 503: Service Unavailable", traceback_text="Traceback:\n  File foo.py\nHTTPError: 503")
            record_error("Connection refused")

            errors = get_recent_errors()
            # Not deduped — each call is a separate entry
            assert len(errors) == 3
            assert errors[0]["error"] == "HTTP 503: Service Unavailable"
            assert errors[0]["traceback"] == "Traceback:\n  File foo.py\nHTTPError: 503"
            assert errors[2]["error"] == "Connection refused"
            assert "traceback" not in errors[2]  # no traceback passed

    def test_heartbeat_errors_are_deduped(self, tmp_path):
        """Heartbeat payload deduplicates errors by message with counts."""
        status_file = tmp_path / "push_status.json"
        lock_file = tmp_path / "push_status.lock"

        with patch("qc_trace.daemon.push_status.PUSH_STATUS_FILE", status_file), \
             patch("qc_trace.daemon.push_status._LOCK_FILE", lock_file):
            from qc_trace.daemon.push_status import record_error, get_recent_errors_for_heartbeat

            record_error("HTTP 503", traceback_text="tb1")
            record_error("HTTP 503", traceback_text="tb2")
            record_error("Connection refused")

            hb_errors = get_recent_errors_for_heartbeat()
            assert len(hb_errors) == 2  # deduped

            e503 = [e for e in hb_errors if "503" in e["error"]][0]
            assert e503["count"] == 2
            assert "traceback" not in e503  # no tracebacks in heartbeat

    def test_old_errors_pruned(self, tmp_path):
        """Errors older than 7 days should be pruned."""
        status_file = tmp_path / "push_status.json"
        lock_file = tmp_path / "push_status.lock"

        with patch("qc_trace.daemon.push_status.PUSH_STATUS_FILE", status_file), \
             patch("qc_trace.daemon.push_status._LOCK_FILE", lock_file):
            from qc_trace.daemon.push_status import get_recent_errors

            # Write an old error directly
            old_ts = time.time() - (8 * 24 * 3600)  # 8 days ago
            data = {
                "recent_errors": [
                    {"ts": old_ts, "error": "Old error"}
                ]
            }
            status_file.parent.mkdir(parents=True, exist_ok=True)
            status_file.write_text(json.dumps(data))

            errors = get_recent_errors()
            assert len(errors) == 0  # pruned

    def test_error_ring_buffer_capped(self, tmp_path):
        """Should not exceed _MAX_RECENT_ERRORS entries."""
        status_file = tmp_path / "push_status.json"
        lock_file = tmp_path / "push_status.lock"

        with patch("qc_trace.daemon.push_status.PUSH_STATUS_FILE", status_file), \
             patch("qc_trace.daemon.push_status._LOCK_FILE", lock_file):
            from qc_trace.daemon.push_status import record_error

            for i in range(30):
                record_error(f"Error {i}")

            data = json.loads(status_file.read_text())
            assert len(data["recent_errors"]) == 20  # capped at _MAX_RECENT_ERRORS

    def test_traceback_stored_in_file(self, tmp_path):
        """Full traceback text is persisted to push_status.json."""
        status_file = tmp_path / "push_status.json"
        lock_file = tmp_path / "push_status.lock"

        with patch("qc_trace.daemon.push_status.PUSH_STATUS_FILE", status_file), \
             patch("qc_trace.daemon.push_status._LOCK_FILE", lock_file):
            from qc_trace.daemon.push_status import record_error

            tb = (
                "Traceback (most recent call last):\n"
                '  File "pusher.py", line 73, in _post_batch\n'
                "    with urllib.request.urlopen(req, timeout=30) as resp:\n"
                "urllib.error.URLError: <urlopen error [Errno 503] Service Unavailable>"
            )
            record_error("HTTP 503", traceback_text=tb)

            data = json.loads(status_file.read_text())
            assert data["recent_errors"][0]["traceback"] == tb
