"""Команды CLI доступны в дашборде — кнопками (R13, R14).

Владелец просил сначала «все команды cli должны быть доступны и в дашборде»
(R17), потом — «в формате кнопок, доп консоль в дашборде web не нужна». Здесь
осталась первая половина заказа: команда исполняется с панели и показывает
вывод. Вторая половина — что своей строки у панели нет и что `run` запускает
только записанную проверку — живёт в `tests/test_buttons_guard.py`.

Проверяются ключевые правила:
1. POST /api/cli выполняет заранее заданный вызов (next, status, doctor и др.)
   и возвращает вывод и код завершения.
2. Нелокальный доступ (с других IP) к /api/cli и /api/commands отбивается 403 (рычаг порчи).
3. Неназванная кнопка или битый JSON возвращают 400 с понятной ошибкой.
4. GET /api/commands отдаёт список команд CLI с описаниями.
5. На главной странице (index.html) есть раздел команд и место под их вывод.
"""

from __future__ import annotations

import json
import socket
import subprocess
import sys
import threading
from functools import partial
from http.client import HTTPConnection
from http.server import ThreadingHTTPServer
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from chekan import dash, serve_app  # noqa: E402
from chekan.store import Run  # noqa: E402


class PanelServer:
    """Локальный тестовый сервер панели."""

    def __init__(self, directory: Path):
        self.directory = directory
        handler = partial(serve_app.Handler, directory=str(directory))
        self.httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
        self.port = self.httpd.server_address[1]
        self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
        self.thread.start()

    def close(self):
        self.httpd.shutdown()
        self.httpd.server_close()

    def post(self, path: str, body: dict | str | None = None, headers: dict | None = None) -> tuple[int, dict]:
        conn = HTTPConnection("127.0.0.1", self.port, timeout=10)
        hdrs = {"Content-Type": "application/json"}
        if headers:
            hdrs.update(headers)
        data = json.dumps(body) if isinstance(body, dict) else (body or "")
        conn.request("POST", path, body=data.encode("utf-8") if isinstance(data, str) else data, headers=hdrs)
        resp = conn.getresponse()
        raw = resp.read().decode("utf-8")
        conn.close()
        try:
            return resp.status, json.loads(raw)
        except ValueError:
            return resp.status, {"raw": raw}

    def get(self, path: str) -> tuple[int, dict | str]:
        conn = HTTPConnection("127.0.0.1", self.port, timeout=10)
        conn.request("GET", path)
        resp = conn.getresponse()
        raw = resp.read().decode("utf-8")
        conn.close()
        try:
            return resp.status, json.loads(raw)
        except ValueError:
            return resp.status, raw


@pytest.fixture
def repo(tmp_path: Path) -> Path:
    root = tmp_path / "proj"
    root.mkdir()
    subprocess.run(["git", "init", "-q"], cwd=root, check=True)
    subprocess.run(["git", "config", "user.email", "t@t"], cwd=root, check=True)
    subprocess.run(["git", "config", "user.name", "t"], cwd=root, check=True)
    (root / "app.py").write_text("print('ok')\n", encoding="utf-8")
    subprocess.run(["git", "add", "-A"], cwd=root, check=True)
    subprocess.run(["git", "commit", "-qm", "init"], cwd=root, check=True)
    return root


@pytest.fixture
def run(repo: Path) -> Run:
    r = Run.create(repo, "Тестовый прогон", "normal", "T1")
    r.add_requirement("R17", "команды в дашборде")
    r.add_task("01", "задача 1", ["R17"], verify="true")
    (repo / ".chekan" / "active").write_text(r.slug + "\n", encoding="utf-8")
    dash.build_index(repo / ".chekan")
    return r


@pytest.fixture
def panel(run: Run):
    srv = PanelServer(run.root / ".chekan")
    try:
        yield srv
    finally:
        srv.close()


# --- Выполнение команд через /api/cli ---


def test_cli_exec_runs_doctor(panel: PanelServer):
    status, data = panel.post("/api/cli", {"action": "doctor"})
    assert status == 200
    assert data["ok"] is True
    assert data["code"] == 0
    assert "git" in data["output"]


def test_cli_exec_runs_status_and_next(panel: PanelServer):
    status, data = panel.post("/api/cli", {"action": "status"})
    assert status == 200
    assert "Тестовый прогон" in data["output"]

    status, data = panel.post("/api/cli", {"action": "next"})
    assert status == 200
    assert "Фаза" in data["output"] or "Подготовка" in data["output"]


def test_cli_exec_unknown_button_returns_400(panel: PanelServer):
    """Рычаг порчи: имени вне списка кнопок панель не исполняет."""
    status, data = panel.post("/api/cli", {"action": "unknown-subcommand-xyz"})
    assert status == 400
    assert "error" in data


def test_cli_exec_unnamed_button_returns_400(panel: PanelServer):
    status, data = panel.post("/api/cli", {"action": ""})
    assert status == 400
    assert "error" in data


def test_cli_exec_bad_json_returns_400(panel: PanelServer):
    conn = HTTPConnection("127.0.0.1", panel.port, timeout=10)
    conn.request("POST", "/api/cli", body=b"not-a-json", headers={"Content-Type": "application/json"})
    resp = conn.getresponse()
    assert resp.status == 400
    conn.close()


# --- Список команд /api/commands ---


def test_get_commands_list(panel: PanelServer):
    status, data = panel.get("/api/commands")
    assert status == 200
    names = [c["name"] for c in data.get("commands", [])]
    for required in ("init", "next", "status", "pause", "resume", "doctor", "board", "accept", "report"):
        assert required in names, f"команда {required} отсутствует в списке"


# --- Безопасность: запрет нелокальных IP ---


def test_non_local_ip_is_forbidden_on_cli_and_commands(monkeypatch, run: Run):
    """Рычаг порчи: запрос с удалённого IP обязан получить 403."""
    root = run.root / ".chekan"
    handler = object.__new__(serve_app.Handler)
    handler.client_address = ("192.168.1.100", 12345)
    handler.directory = str(root)
    written = []

    def fake_json(code, payload):
        written.append((code, payload))

    monkeypatch.setattr(handler, "_json", fake_json)
    handler.path = "/api/cli"
    handler.do_POST()
    assert written and written[0][0] == 403

    written.clear()
    handler.path = "/api/commands"
    handler.do_GET()
    assert written and written[0][0] == 403


# --- Разметка на index.html ---


def test_index_html_contains_cli_section(run: Run):
    index_path = run.root / ".chekan" / "index.html"
    html = index_path.read_text(encoding="utf-8")
    assert 'id="clicmds"' in html
    assert "Команды CLI" in html
    assert 'id="clilist"' in html          # сюда страница рисует список от сервера
    assert 'id="cliout"' in html           # сюда ложится вывод команды
