"""Tests for daemon HTTP pusher."""

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

import pytest

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

from tests.helpers import make_message


class TestSerializeMessage:
    def test_basic_serialization(self):
        msg = make_message()
        d = _serialize_message(msg)
        assert d["id"] == "msg-1"
        assert d["source"] == "claude_code"
        assert d["content"] == "hello"
        # None fields should be removed
        assert "thinking" not in d
        assert "tool_call" not in d

    def test_json_roundtrip(self):
        msg = make_message()
        d = _serialize_message(msg)
        s = json.dumps(d)
        restored = json.loads(s)
        assert restored["id"] == "msg-1"


class TestPusher:
    @pytest.fixture
    def config(self) -> DaemonConfig:
        return DaemonConfig(
            ingest_url="http://localhost:19999/ingest",
            retry_backoff_base=0.01,
            retry_backoff_max=0.1,
            retry_queue_max=100,
            batch_size=10,
        )

    @pytest.fixture
    def pusher(self, config: DaemonConfig) -> Pusher:
        return Pusher(config=config)

    def test_push_empty_list(self, pusher: Pusher):
        assert pusher.push([]) is True

    def test_push_failure_queues_messages(self, pusher: Pusher):
        msgs = [make_message(id=f"msg-{i}") for i in range(5)]
        # No server running, so push should fail
        result = pusher.push(msgs)
        assert result is False
        assert pusher.queue_size == 5
        assert pusher.has_queued()

    def test_backoff_increases(self, pusher: Pusher):
        msgs = [make_message()]
        pusher.push(msgs)
        backoff1 = pusher.current_backoff

        pusher.push(msgs)
        backoff2 = pusher.current_backoff

        assert backoff2 > backoff1

    def test_queue_bounded(self, pusher: Pusher):
        pusher.config.retry_queue_max = 5
        msgs = [make_message(id=f"msg-{i}") for i in range(10)]
        pusher.push(msgs)
        assert pusher.queue_size == 5  # oldest 5 dropped

    def test_success_resets_backoff(self, pusher: Pusher):
        # Simulate failure
        msgs = [make_message()]
        pusher.push(msgs)
        assert pusher.current_backoff > 0

        # Reset on success
        pusher._on_success()
        assert pusher.current_backoff == 0.0
        assert pusher._consecutive_failures == 0


class TestPusherWithServer:
    """Integration tests with a real HTTP server."""

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

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

            def log_message(self, format, *args):
                pass  # suppress logs

        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, progress_received
        srv.shutdown()

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

        msgs = [make_message(id="msg-1"), make_message(id="msg-2")]
        result = pusher.push(msgs)
        assert result is True
        assert len(received) == 1
        assert len(received[0]) == 2

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

        # Pre-fill queue
        pusher._queue.extend(
            [_serialize_message(make_message(id=f"queued-{i}")) for i in range(3)]
        )

        # Push new messages — should also drain queue
        msgs = [make_message(id="new-1")]
        result = pusher.push(msgs)
        assert result is True
        assert len(received) == 2  # one for new, one for queued
        assert pusher.queue_size == 0

    def test_new_messages_sent_before_queued(self, server):
        """New messages are pushed first, then queued messages are drained (M2)."""
        port, received, _ = server
        config = DaemonConfig(
            ingest_url=f"http://127.0.0.1:{port}/ingest",
            batch_size=100,
        )
        pusher = Pusher(config=config)

        # Pre-fill queue with identifiable messages
        pusher._queue.extend(
            [_serialize_message(make_message(id=f"queued-{i}")) for i in range(2)]
        )

        # Push new messages
        msgs = [make_message(id="new-1"), make_message(id="new-2")]
        result = pusher.push(msgs)
        assert result is True
        assert len(received) == 2

        # First batch should be the new messages
        new_ids = [m["id"] for m in received[0]]
        assert new_ids == ["new-1", "new-2"]

        # Second batch should be the queued messages
        queued_ids = [m["id"] for m in received[1]]
        assert queued_ids == ["queued-0", "queued-1"]

    def test_drain_failure_requeues_messages(self, server):
        """If draining the queue fails, messages are re-queued (M2)."""
        port, received, _ = server
        config = DaemonConfig(
            ingest_url=f"http://127.0.0.1:{port}/ingest",
            batch_size=100,
        )
        pusher = Pusher(config=config)

        # Push succeeds for new messages
        msgs = [make_message(id="new-1")]
        result = pusher.push(msgs)
        assert result is True

        # Now pre-fill queue and make server unavailable for drain
        pusher._queue.extend(
            [_serialize_message(make_message(id=f"queued-{i}")) for i in range(3)]
        )

        # Patch _post_batch to fail only for the drain call
        original_post = pusher._post_batch
        call_count = [0]

        def patched_post(batch):
            call_count[0] += 1
            if call_count[0] == 2:  # fail on drain
                return False
            return original_post(batch)

        pusher._post_batch = patched_post
        msgs = [make_message(id="new-2")]
        pusher.push(msgs)

        # Queue should still have the messages since drain failed
        assert pusher.queue_size == 3

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

        result = pusher.report_progress(
            file_path="/tmp/test.jsonl",
            source="claude_code",
            last_line_read=150,
            content_hash="abc123",
        )
        assert result is True
        assert len(progress_received) == 1
        assert progress_received[0]["raw_file_path"] == "/tmp/test.jsonl"
        assert progress_received[0]["last_line_read"] == 150
        assert progress_received[0]["content_hash"] == "abc123"

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

        result = pusher.report_progress(
            file_path="/tmp/test.jsonl",
            source="claude_code",
            last_line_read=100,
        )
        assert result is True
        assert progress_received[0]["content_hash"] is None

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

        result = pusher.report_progress(
            file_path="/tmp/test.jsonl",
            source="claude_code",
            last_line_read=100,
        )
        assert result is False
