"""Tests for reingest command feature — state resets, daemon command handling, pusher."""

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

import pytest

from qc_trace.daemon.config import DaemonConfig
from qc_trace.daemon.pusher import Pusher
from qc_trace.daemon.state import FileState, StateManager


# ---------------------------------------------------------------------------
# StateManager reset method tests
# ---------------------------------------------------------------------------


class TestStateManagerResets:
    """Test reset_all, reset_by_source, reset_since."""

    def _make_state(self, tmp_path: Path) -> StateManager:
        mgr = StateManager(state_file=tmp_path / "state.json")
        mgr.set_state(FileState(
            file_path="/home/user/.claude/sessions/a.jsonl",
            source="claude_code",
            last_line_processed=100,
            last_mtime=1708300000.0,
        ))
        mgr.set_state(FileState(
            file_path="/home/user/.claude/sessions/b.jsonl",
            source="claude_code",
            last_line_processed=50,
            last_mtime=1708400000.0,
        ))
        mgr.set_state(FileState(
            file_path="/home/user/.codex/history/c.jsonl",
            source="codex_cli",
            last_line_processed=30,
            last_mtime=1708200000.0,
        ))
        mgr.set_state(FileState(
            file_path="/home/user/.gemini/sessions/d.json",
            source="gemini_cli",
            content_hash="abc123",
            last_mtime=1708350000.0,
        ))
        return mgr

    def test_reset_all(self, tmp_path):
        mgr = self._make_state(tmp_path)
        assert len(mgr.all_states()) == 4
        count = mgr.reset_all()
        assert count == 4
        assert len(mgr.all_states()) == 0

    def test_reset_all_empty(self, tmp_path):
        mgr = StateManager(state_file=tmp_path / "state.json")
        count = mgr.reset_all()
        assert count == 0

    def test_reset_by_source(self, tmp_path):
        mgr = self._make_state(tmp_path)
        count = mgr.reset_by_source("claude_code")
        assert count == 2
        remaining = mgr.all_states()
        assert len(remaining) == 2
        assert all(fs.source != "claude_code" for fs in remaining.values())

    def test_reset_by_source_no_match(self, tmp_path):
        mgr = self._make_state(tmp_path)
        count = mgr.reset_by_source("cursor")
        assert count == 0
        assert len(mgr.all_states()) == 4

    def test_reset_since(self, tmp_path):
        mgr = self._make_state(tmp_path)
        # cutoff between c.jsonl (1708200000) and a.jsonl (1708300000)
        from datetime import datetime, timezone
        cutoff_dt = datetime.fromtimestamp(1708300000, tz=timezone.utc)
        count = mgr.reset_since(cutoff_dt.isoformat())
        # Should remove a (1708300000), b (1708400000), d (1708350000)
        assert count == 3
        remaining = mgr.all_states()
        assert len(remaining) == 1
        assert list(remaining.values())[0].source == "codex_cli"

    def test_reset_since_invalid_date(self, tmp_path):
        mgr = self._make_state(tmp_path)
        count = mgr.reset_since("not-a-date")
        assert count == 0
        assert len(mgr.all_states()) == 4

    def test_reset_state_single_file(self, tmp_path):
        mgr = self._make_state(tmp_path)
        mgr.reset_state("/home/user/.claude/sessions/a.jsonl")
        assert mgr.get_state("/home/user/.claude/sessions/a.jsonl") is None
        assert len(mgr.all_states()) == 3


# ---------------------------------------------------------------------------
# Pusher.send_heartbeat return type + report_command_complete
# ---------------------------------------------------------------------------


class TestPusherCommandMethods:
    """Test updated send_heartbeat return type and report_command_complete."""

    @pytest.fixture
    def command_server(self):
        """Server that tracks received requests and can return commands."""
        received = []
        commands_to_return = []

        class Handler(BaseHTTPRequestHandler):
            def do_POST(self):
                length = int(self.headers.get("Content-Length", 0))
                body = self.rfile.read(length)
                received.append({
                    "path": self.path,
                    "body": json.loads(body),
                })
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.end_headers()
                if self.path == "/api/heartbeat":
                    resp = {"ok": True, "commands": list(commands_to_return)}
                else:
                    resp = {"ok": True}
                self.wfile.write(json.dumps(resp).encode())

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

    def test_send_heartbeat_returns_dict_with_commands(self, command_server):
        port, received, commands_to_return = command_server
        commands_to_return.append({
            "id": 42,
            "command": "reingest",
            "params": {"scope": "all"},
        })

        config = DaemonConfig(ingest_url=f"http://127.0.0.1:{port}/ingest")
        pusher = Pusher(config=config)

        result = pusher.send_heartbeat({"device_id": "x", "status": "idle"})
        assert isinstance(result, dict)
        assert result["ok"] is True
        assert len(result["commands"]) == 1
        assert result["commands"][0]["id"] == 42

    def test_send_heartbeat_returns_none_on_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_empty_commands(self, command_server):
        port, received, _ = command_server
        config = DaemonConfig(ingest_url=f"http://127.0.0.1:{port}/ingest")
        pusher = Pusher(config=config)

        result = pusher.send_heartbeat({"device_id": "x", "status": "idle"})
        assert result["commands"] == []

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

        ok = pusher.report_command_complete(
            42, "completed", {"files_reset": 5, "messages_pushed": 100}
        )
        assert ok is True
        assert len(received) == 1
        assert received[0]["path"] == "/api/command-complete"
        assert received[0]["body"]["command_id"] == 42
        assert received[0]["body"]["status"] == "completed"

    def test_report_command_complete_failure(self):
        config = DaemonConfig(ingest_url="http://127.0.0.1:19999/ingest")
        pusher = Pusher(config=config)
        ok = pusher.report_command_complete(1, "failed", {"error": "boom"})
        assert ok is False


# ---------------------------------------------------------------------------
# Daemon command dispatch tests
# ---------------------------------------------------------------------------


class TestDaemonCommandHandling:
    """Test Daemon._handle_commands and _handle_reingest."""

    def _make_daemon(self, tmp_path):
        from qc_trace.daemon.main import Daemon
        config = DaemonConfig(
            ingest_url="http://127.0.0.1:19999/ingest",
            base_dir=tmp_path,
        )
        daemon = Daemon(config=config)
        daemon.device_name = "test-host"
        daemon.device_id = "test-device"
        return daemon

    def test_handle_commands_unknown_command(self, tmp_path):
        daemon = self._make_daemon(tmp_path)
        daemon.pusher.report_command_complete = MagicMock(return_value=True)

        daemon._handle_commands([{
            "id": 1,
            "command": "unknown_cmd",
            "params": {},
        }])

        daemon.pusher.report_command_complete.assert_called_once_with(
            1, "failed", {"error": "Unknown command: unknown_cmd"}
        )

    def test_handle_reingest_resets_all_state(self, tmp_path):
        daemon = self._make_daemon(tmp_path)
        daemon.state_mgr.set_state(FileState(
            file_path="/a.jsonl", source="claude_code", last_line_processed=100,
        ))
        daemon.state_mgr.set_state(FileState(
            file_path="/b.jsonl", source="codex_cli", last_line_processed=50,
        ))

        # Mock watcher and pusher to avoid real I/O
        daemon.watcher.get_changed_files = MagicMock(return_value=[])
        daemon.pusher.report_command_complete = MagicMock(return_value=True)
        daemon.pusher.report_progress_bulk = MagicMock(return_value=True)

        daemon._handle_reingest(42, {"scope": "all"})

        assert len(daemon.state_mgr.all_states()) == 0
        daemon.pusher.report_command_complete.assert_called_once()
        call_args = daemon.pusher.report_command_complete.call_args
        assert call_args[0][0] == 42
        assert call_args[0][1] == "completed"
        assert call_args[0][2]["files_reset"] == 2

    def test_handle_reingest_by_source(self, tmp_path):
        daemon = self._make_daemon(tmp_path)
        daemon.state_mgr.set_state(FileState(
            file_path="/a.jsonl", source="claude_code", last_line_processed=100,
        ))
        daemon.state_mgr.set_state(FileState(
            file_path="/b.jsonl", source="codex_cli", last_line_processed=50,
        ))

        daemon.watcher.get_changed_files = MagicMock(return_value=[])
        daemon.pusher.report_command_complete = MagicMock(return_value=True)

        daemon._handle_reingest(43, {"source": "claude_code"})

        assert len(daemon.state_mgr.all_states()) == 1
        remaining = list(daemon.state_mgr.all_states().values())[0]
        assert remaining.source == "codex_cli"

    def test_handle_reingest_single_file(self, tmp_path):
        daemon = self._make_daemon(tmp_path)
        daemon.state_mgr.set_state(FileState(
            file_path="/a.jsonl", source="claude_code", last_line_processed=100,
        ))
        daemon.state_mgr.set_state(FileState(
            file_path="/b.jsonl", source="codex_cli", last_line_processed=50,
        ))

        daemon.watcher.get_changed_files = MagicMock(return_value=[])
        daemon.pusher.report_command_complete = MagicMock(return_value=True)

        daemon._handle_reingest(44, {"file": "/a.jsonl"})

        assert daemon.state_mgr.get_state("/a.jsonl") is None
        assert daemon.state_mgr.get_state("/b.jsonl") is not None

    def test_handle_reingest_failure_reports_error(self, tmp_path):
        daemon = self._make_daemon(tmp_path)

        # Force an error by making state_mgr.reset_all raise
        daemon.state_mgr.reset_all = MagicMock(side_effect=RuntimeError("boom"))
        daemon.pusher.report_command_complete = MagicMock(return_value=True)

        daemon._handle_reingest(45, {"scope": "all"})

        daemon.pusher.report_command_complete.assert_called_once()
        call_args = daemon.pusher.report_command_complete.call_args
        assert call_args[0][1] == "failed"
        assert "boom" in call_args[0][2]["error"]

    def test_paused_flag_always_cleared(self, tmp_path):
        """Verify _paused is always False after _handle_commands, even on error."""
        daemon = self._make_daemon(tmp_path)
        daemon._paused = True

        # Force error in reingest
        daemon.state_mgr.reset_all = MagicMock(side_effect=RuntimeError("fail"))
        daemon.pusher.report_command_complete = MagicMock(return_value=True)
        daemon.watcher.get_changed_files = MagicMock(return_value=[])

        # Simulate the run() loop's command handling with try/finally
        daemon._pending_commands = [{"id": 1, "command": "reingest", "params": {"scope": "all"}}]
        try:
            daemon._handle_commands(daemon._pending_commands)
        finally:
            daemon._pending_commands = []
            daemon._paused = False

        assert daemon._paused is False
        assert daemon._pending_commands == []
