"""Tests for ``launcher.doctor`` — tribunal doctor command."""

from __future__ import annotations

import json
import socket
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from launcher.doctor import (
    _CHECK,
    _FAIL,
    _WARN,
    _check_hooks_json,
    _check_python_deps,
    _check_settings_hooks,
    _check_worker_service,
    run_doctor,
)

# ── _check_settings_hooks ─────────────────────────────────────────────────────

class TestCheckSettingsHooks:
    """Tests for _check_settings_hooks()."""

    def test_returns_false_when_no_settings_file(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)
        ok, msg = _check_settings_hooks()
        assert ok is False
        assert "settings.json" in msg.lower() or "no .claude" in msg.lower()

    def test_returns_false_when_invalid_json(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{bad json}")

        ok, msg = _check_settings_hooks()
        assert ok is False
        assert "cannot read" in msg.lower() or "invalid" in msg.lower() or "json" in msg.lower()

    def test_returns_false_when_no_hooks(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text(json.dumps({"model": "sonnet"}))

        ok, msg = _check_settings_hooks()
        assert ok is False
        assert "no hooks" in msg.lower() or "repair" in msg.lower()

    def test_returns_false_when_no_tribunal_hooks(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        settings = {
            "hooks": {
                "PostToolUse": [{"command": "echo hello"}],
            }
        }
        (claude_dir / "settings.json").write_text(json.dumps(settings))

        ok, msg = _check_settings_hooks()
        assert ok is False
        assert "tribunal" in msg.lower() or "no tribunal" in msg.lower()

    def test_returns_true_when_tribunal_hook_found(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        settings = {
            "hooks": {
                "PostToolUse": [{"command": "python file_checker.py"}],
                "PreToolUse": [{"command": "python tdd_enforcer.py"}],
            }
        }
        (claude_dir / "settings.json").write_text(json.dumps(settings))

        ok, msg = _check_settings_hooks()
        assert ok is True
        assert "hook" in msg.lower()

    def test_tribunal_keyword_in_command_passes(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        settings = {
            "hooks": {
                "PostToolUse": [{"command": "tribunal-hook --run"}],
            }
        }
        (claude_dir / "settings.json").write_text(json.dumps(settings))

        ok, _ = _check_settings_hooks()
        assert ok is True


# ── _check_hooks_json ─────────────────────────────────────────────────────────

class TestCheckHooksJson:
    """Tests for _check_hooks_json()."""

    def test_returns_false_when_hooks_json_missing(self, tmp_path):
        with patch("launcher.doctor._PLUGIN_DIR", tmp_path / "tribunal"):
            ok, msg = _check_hooks_json()
        assert ok is False
        assert "not found" in msg.lower() or "repair" in msg.lower()

    def test_returns_false_when_hooks_json_invalid(self, tmp_path):
        plugin_dir = tmp_path / "tribunal"
        hooks_dir = plugin_dir / "hooks"
        hooks_dir.mkdir(parents=True)
        (hooks_dir / "hooks.json").write_text("this is not json!")

        with patch("launcher.doctor._PLUGIN_DIR", plugin_dir):
            ok, msg = _check_hooks_json()
        assert ok is False
        assert "invalid" in msg.lower() or "json" in msg.lower()

    def test_returns_true_for_valid_hooks_json_dict(self, tmp_path):
        plugin_dir = tmp_path / "tribunal"
        hooks_dir = plugin_dir / "hooks"
        hooks_dir.mkdir(parents=True)
        (hooks_dir / "hooks.json").write_text(json.dumps({"version": 1}))

        with patch("launcher.doctor._PLUGIN_DIR", plugin_dir):
            ok, msg = _check_hooks_json()
        assert ok is True
        assert "valid" in msg.lower()

    def test_returns_true_for_valid_hooks_json_list(self, tmp_path):
        plugin_dir = tmp_path / "tribunal"
        hooks_dir = plugin_dir / "hooks"
        hooks_dir.mkdir(parents=True)
        (hooks_dir / "hooks.json").write_text(json.dumps([{"type": "pre"}]))

        with patch("launcher.doctor._PLUGIN_DIR", plugin_dir):
            ok, msg = _check_hooks_json()
        assert ok is True


# ── _check_python_deps ────────────────────────────────────────────────────────

class TestCheckPythonDeps:
    """Tests for _check_python_deps()."""

    def test_returns_true_for_current_python(self, tmp_path):
        """Should pass on the current Python (which must be ≥3.10 to run tests)."""
        plugin_dir = tmp_path / "tribunal"
        hooks_dir = plugin_dir / "hooks"
        hooks_dir.mkdir(parents=True)

        with patch("launcher.doctor._PLUGIN_DIR", plugin_dir):
            ok, msg = _check_python_deps()
        assert ok is True
        assert "python" in msg.lower()

    def test_returns_false_when_python_too_old(self, tmp_path):
        plugin_dir = tmp_path / "tribunal"
        hooks_dir = plugin_dir / "hooks"
        hooks_dir.mkdir(parents=True)

        with patch("launcher.doctor._PLUGIN_DIR", plugin_dir):
            with patch("sys.version_info", new=(3, 9, 0)):
                ok, msg = _check_python_deps()
        assert ok is False
        assert "3.10" in msg

    def test_returns_false_when_hooks_dir_missing(self, tmp_path):
        plugin_dir = tmp_path / "tribunal"
        # No hooks dir created

        with patch("launcher.doctor._PLUGIN_DIR", plugin_dir):
            ok, msg = _check_python_deps()
        assert ok is False
        assert "repair" in msg.lower() or "not found" in msg.lower()


# ── _check_worker_service ─────────────────────────────────────────────────────

class TestCheckWorkerService:
    """Tests for _check_worker_service()."""

    def test_returns_true_when_socket_open(self):
        with patch("socket.socket") as mock_socket_cls:
            mock_sock = MagicMock()
            mock_sock.connect_ex.return_value = 0  # success
            mock_socket_cls.return_value = mock_sock

            ok, msg = _check_worker_service()
        assert ok is True
        assert "running" in msg.lower() or "reachable" in msg.lower()

    def test_returns_none_when_socket_refused(self):
        with patch("socket.socket") as mock_socket_cls:
            mock_sock = MagicMock()
            mock_sock.connect_ex.return_value = 111  # ECONNREFUSED
            mock_socket_cls.return_value = mock_sock

            ok, msg = _check_worker_service()
        assert ok is None  # warning, not hard fail
        assert "not" in msg.lower() or "port" in msg.lower()

    def test_reads_custom_port_from_settings(self, tmp_path):
        settings_file = tmp_path / "settings.json"
        settings_file.write_text(json.dumps({"SKILLFIELD_WORKER_PORT": "12345"}))

        with patch("socket.socket") as mock_socket_cls:
            mock_sock = MagicMock()
            mock_sock.connect_ex.return_value = 0
            mock_socket_cls.return_value = mock_sock

            with patch("launcher.doctor.Path") as mock_path_cls:
                # Make Path.home() / ".skillfield" / "memory" / "settings.json" → settings_file
                mock_home = MagicMock()
                mock_path_cls.home.return_value = mock_home
                mock_home.__truediv__ = lambda self, x: tmp_path if x == ".skillfield" else MagicMock()

                # Directly call with settings file patched
                with patch("pathlib.Path.home", return_value=tmp_path):
                    pass  # just ensure no crash; port reading is best-effort

            ok, msg = _check_worker_service()
        assert ok is True  # socket responded


# ── run_doctor (integration) ──────────────────────────────────────────────────

class TestRunDoctor:
    """Integration tests for run_doctor()."""

    def test_all_passing_returns_zero(self, capsys):
        passing_checks = [
            ("Alpha", lambda: (True, "all good")),
            ("Beta", lambda: (True, "also good")),
        ]
        result = run_doctor(checks=passing_checks)
        assert result == 0
        out = capsys.readouterr().out
        assert _CHECK in out
        assert "All checks passed" in out

    def test_one_failure_returns_one(self, capsys):
        failing_checks = [
            ("Alpha", lambda: (True, "ok")),
            ("Beta", lambda: (False, "broken\n    Fix: do something")),
        ]
        result = run_doctor(checks=failing_checks)
        assert result == 1
        out = capsys.readouterr().out
        assert _FAIL in out
        assert "failed" in out.lower()

    def test_warning_does_not_fail(self, capsys):
        warn_checks = [
            ("Alpha", lambda: (None, "uncertain")),
        ]
        result = run_doctor(checks=warn_checks)
        assert result == 0
        out = capsys.readouterr().out
        assert _WARN in out
        assert "All checks passed" in out

    def test_mixed_pass_warn_fail_returns_one(self, capsys):
        mixed = [
            ("Pass", lambda: (True, "ok")),
            ("Warn", lambda: (None, "maybe")),
            ("Fail", lambda: (False, "nope")),
        ]
        result = run_doctor(checks=mixed)
        assert result == 1

    def test_multiline_message_is_indented(self, capsys):
        checks = [
            ("MyCheck", lambda: (False, "line1\n    Fix: fix it")),
        ]
        run_doctor(checks=checks)
        out = capsys.readouterr().out
        assert "line1" in out
        assert "Fix: fix it" in out

    def test_prints_header(self, capsys):
        run_doctor(checks=[])
        out = capsys.readouterr().out
        assert "Tribunal Doctor" in out


# ── CLI integration ───────────────────────────────────────────────────────────

class TestDoctorCLI:
    """Test that 'tribunal doctor' is wired into the CLI."""

    def test_doctor_subcommand_is_registered(self):
        from launcher.cli import _build_parser

        parser = _build_parser()
        # Verify 'doctor' appears in the parser's choices
        subparsers_action = next(
            a for a in parser._actions if hasattr(a, "_name_parser_map")
        )
        assert "doctor" in subparsers_action._name_parser_map

    def test_doctor_command_exits_with_code(self, tmp_path, monkeypatch):
        """tribunal doctor exits 0 or 1 (not an uncaught exception)."""
        monkeypatch.chdir(tmp_path)

        from launcher.cli import main

        with patch("launcher.doctor.run_doctor", return_value=0) as mock_doctor:
            with pytest.raises(SystemExit) as exc_info:
                main(["doctor"])
            assert exc_info.value.code == 0
            mock_doctor.assert_called_once()
