"""Рычаг порчи (R03).

Правило «проверка обязана краснеть, когда её обманывают» жило в промптах, и его
обходили. Здесь оно становится частью инструмента: `chekan run --lever` требует от
рычага ненулевого кода, пишет его в журнал рядом с прогоном, а отчёт называет
проверки, которых никто не ломал.

Рычаги порчи на саму эту работу — каждый обязан краснеть:
1. Засчитать рычаг с нулевым кодом (снять проверку `runner.is_red` в `cmd_run`).
2. Не писать рычаг в журнал (убрать `run.append_evidence` из `runner.run_lever`).
3. Считать в отчёте не то (считать покрытием любой рычаг, хоть зелёный, — или
   потерять из перечисления непокрытую проверку).
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

import pytest

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

from chekan import proof, report, runner  # noqa: E402
from chekan.__main__ import main  # noqa: E402
from chekan.store import Run, StateError  # noqa: E402


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


@pytest.fixture
def run(repo: Path) -> Run:
    r = Run.create(repo, "Тестовый прогон", "normal", "T1")
    r.add_requirement("R03", "рычаг порчи", note="причина")
    r.add_task("01", "Задача с проверкой", ["R03"], verify="true")
    r.add_task("02", "Вторая задача", ["R03"], verify="true")
    return r


def cli(repo: Path, *args: str) -> int:
    cwd = Path.cwd()
    try:
        os.chdir(repo)
        return main(list(args))
    finally:
        os.chdir(cwd)


def journal(run: Run) -> list[dict]:
    return [
        json.loads(line)
        for line in run.evidence_path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]


# --- отказ на рычаге, который не покраснел ---


def test_zero_exit_lever_is_refused(run: Run, repo: Path, capsys):
    """Порча внесена, проверка зелёная — значит проверяет не то."""
    assert cli(repo, "run", "true", "--task", "01", "--lever", "true") == 1
    captured = capsys.readouterr()
    text = captured.out + captured.err
    assert "не краснеет" in text, text


def test_red_lever_lets_the_run_pass(run: Run, repo: Path):
    assert cli(repo, "run", "true", "--task", "01", "--lever", "false") == 0


def test_run_without_lever_works_as_before(run: Run, repo: Path):
    """Рычаг необязателен: закрывать без него пока можно, просто это видно в отчёте."""
    assert cli(repo, "run", "true", "--task", "01") == 0
    assert all(row["kind"] != runner.LEVER_KIND for row in journal(Run.load(repo)))


def test_empty_lever_is_refused(run: Run, repo: Path, capsys):
    """Пустая строка вернёт ноль и засчиталась бы как «сломали и проверили»."""
    assert cli(repo, "run", "true", "--task", "01", "--lever", "   ") == 0
    with pytest.raises(StateError):
        runner.run_lever(run, "  ", {"cmd": "true", "ts": "x", "exit": 0}, task="01")


def test_lever_is_not_run_when_the_check_itself_is_red(run: Run, repo: Path):
    assert cli(repo, "run", "false", "--task", "01", "--lever", "false") == 1
    assert all(row["kind"] != runner.LEVER_KIND for row in journal(Run.load(repo)))


# --- запись рядом с прогоном ---


def test_lever_is_recorded_as_a_pair_with_the_run(run: Run, repo: Path):
    """В журнале зелёный прогон и красный рычаг видны одной парой."""
    assert cli(repo, "run", "true", "--task", "01", "--lever", "false") == 0
    rows = journal(Run.load(repo))
    check = [r for r in rows if r["kind"] == "check"]
    levers = [r for r in rows if r["kind"] == runner.LEVER_KIND]
    assert len(check) == 1 and len(levers) == 1
    assert check[0]["exit"] == 0
    assert levers[0]["exit"] != 0
    assert levers[0]["task"] == "01"
    assert levers[0]["lever_for"]["cmd"] == check[0]["cmd"]
    assert levers[0]["lever_for"]["ts"] == check[0]["ts"]


def test_lever_never_becomes_the_proof_of_a_task(run: Run, repo: Path):
    """Красный рычаг — не провал задачи: доказательством остаётся зелёный прогон."""
    assert cli(repo, "run", "true", "--task", "01", "--lever", "false") == 0
    reloaded = Run.load(repo)
    green = reloaded.green_evidence("01")
    assert green is not None and green["kind"] == "check"
    ok, messages = proof.close_task(reloaded, "01")
    assert ok, messages


# --- строка отчёта ---


def line(run: Run) -> str:
    text = report.build(run)
    found = [ln for ln in text.splitlines() if "без рычага порчи" in ln]
    assert len(found) == 1, text
    return found[0]


def test_report_counts_and_names_checks_without_lever(run: Run, repo: Path):
    cli(repo, "run", "true", "--task", "01", "--lever", "false")
    cli(repo, "run", "echo второе", "--task", "02")
    text = line(Run.load(repo))
    assert "Проверок без рычага порчи: 1" in text
    assert "echo второе" in text and "02" in text
    assert "`true`" not in text


def test_report_says_zero_when_every_check_is_covered(run: Run, repo: Path):
    cli(repo, "run", "true", "--task", "01", "--lever", "false")
    cli(repo, "run", "echo второе", "--task", "02", "--lever", "false")
    assert "Проверок без рычага порчи: 0" in line(Run.load(repo))


def test_every_bare_check_is_named_not_just_the_first(run: Run, repo: Path):
    """Счётчик без полного перечисления врёт тише, но так же: назван каждый."""
    cli(repo, "run", "echo первое", "--task", "01")
    cli(repo, "run", "echo второе", "--task", "02")
    cli(repo, "run", "true", "--task", "01", "--lever", "false")
    text = line(Run.load(repo))
    assert "Проверок без рычага порчи: 2" in text
    assert "echo первое" in text and "echo второе" in text


def test_green_lever_is_not_counted_as_coverage(run: Run, repo: Path):
    """Рычаг, который не покраснел, ничего не доказал — проверка остаётся голой."""
    cli(repo, "run", "true", "--task", "01", "--lever", "true")
    text = line(Run.load(repo))
    assert "Проверок без рычага порчи: 1" in text
    assert "`true`" in text


def test_lever_covers_only_its_own_check(run: Run, repo: Path):
    """Ссылка на команду обязательна: иначе один рычаг покрыл бы весь журнал."""
    cli(repo, "run", "true", "--task", "01", "--lever", "false")
    cli(repo, "run", "echo другое", "--task", "01")
    missing = runner.checks_without_lever(Run.load(repo))
    assert [m["cmd"] for m in missing] == ["echo другое"]


def test_same_check_is_counted_once(run: Run, repo: Path):
    cli(repo, "run", "echo второе", "--task", "02")
    cli(repo, "run", "echo второе", "--task", "02")
    assert len(runner.checks_without_lever(Run.load(repo))) == 1


def test_red_checks_are_not_counted(run: Run, repo: Path):
    """Красный прогон ничего не доказал — рычаг на нём не нужен."""
    cli(repo, "run", "false", "--task", "01")
    assert runner.checks_without_lever(Run.load(repo)) == []


def test_acceptance_runs_are_not_counted_as_checks(run: Run, repo: Path):
    """Приёмка гоняется профильным скиллом, а не командой задачи: ломать нечего."""
    cli(repo, "run", "true", "--task", "01", "--kind", "accept")
    assert runner.checks_without_lever(Run.load(repo)) == []
