"""Защиты, найденные злонамеренной проверкой.

Каждый тест закрывает конкретный способ обмануть инструмент или уронить его
некрасиво. Все они когда-то проходили — то есть весь смысл «готово доказывается
исполнением» обходился двумя символами `""`.
"""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

import pytest

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

from test_core import repo, run  # noqa: F401,E402 — общие фикстуры

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

ROOT = Path(__file__).resolve().parent.parent


def prepared(run: Run, tid: str = "01") -> Run:  # noqa: F811
    run.add_requirement("R01", "цитата", note="причина")
    run.add_task(tid, "Таск", ["R01"], verify="true")
    return run


# --- ложные доказательства ---


def test_empty_command_is_not_evidence(run: Run):  # noqa: F811
    prepared(run)
    for bogus in ("", "   ", "\t\n"):
        with pytest.raises(StateError, match="пустая команда"):
            proof.run_command(run, bogus, task="01")
    assert not run.evidence("01")


def test_evidence_cannot_precede_the_task(run: Run):  # noqa: F811
    """Доказательство на несуществующий таск позволяло закрыть его задним числом."""
    run.add_requirement("R01", "цитата", note="причина")
    with pytest.raises(StateError, match="нет таска"):
        proof.run_command(run, "true", task="99")


def test_uncommitted_change_after_evidence_is_caught(run: Run, repo: Path):  # noqa: F811
    """Сравнения коммитов мало: обычный прогон коммитит в конце."""
    prepared(run)
    proof.run_command(run, "true", task="01")
    (repo / "app.py").write_text("сломано после доказательства\n", encoding="utf-8")
    ok, messages = proof.close_task(run, "01")
    assert not ok
    assert "рабочая копия изменилась" in " ".join(messages)


# --- целостность состояния ---


def test_parallel_writes_do_not_lose_records(run: Run, repo: Path):  # noqa: F811
    """Без блокировки одновременные записи затирали друг друга, рапортуя успех."""
    env = {"PYTHONPATH": str(ROOT), "PATH": "/usr/bin:/bin", "HOME": str(repo)}
    procs = [
        subprocess.Popen(
            [sys.executable, "-m", "chekan", "req", "add", f"R{i:02d}",
             "--quote", "цитата", "--note", "причина"],
            cwd=repo, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        )
        for i in range(10, 22)
    ]
    for p in procs:
        p.wait(timeout=60)
    ids = {r["id"] for r in Run(repo, run.slug).state["requirements"]}
    expected = {f"R{i:02d}" for i in range(10, 22)}
    assert expected <= ids, f"потеряны: {sorted(expected - ids)}"


def test_active_pointer_cannot_escape_the_run_dir(run: Run, repo: Path):  # noqa: F811
    """Слаг подставляется в путь — «..» уводил запись за пределы каталога."""
    (repo / ".chekan" / "active").write_text("../OUTSIDE\n", encoding="utf-8")
    assert Run.active_slug(repo) is None


def test_broken_gate_payload_is_refused(run: Run, repo: Path):  # noqa: F811
    """Валидный JSON чужой формы кирпичил прогон: команды переставали читать состояние."""
    bad = repo / "verdict.json"
    bad.write_text('["ok"]', encoding="utf-8")
    assert main(["gate", "G4", "--blind", str(bad)]) == 1
    assert "blind" not in run.state.get("gates", {}).get("G4", {})


def test_missing_gate_payload_is_refused(run: Run, repo: Path):  # noqa: F811
    assert main(["gate", "G4", "--blind", str(repo / "нет-такого.json")]) == 1


# --- закрытый прогон ---


def test_finished_run_refuses_mutations(run: Run, repo: Path):  # noqa: F811
    """Сданный отчёт устаревал молча: правки принимались и в него не попадали."""
    prepared(run)
    st = run.state
    st["finished_at"] = "2026-01-01T00:00:00+03:00"
    run.save(st)

    env = {"PYTHONPATH": str(ROOT), "PATH": "/usr/bin:/bin", "HOME": str(repo)}
    out = subprocess.run(
        [sys.executable, "-m", "chekan", "req", "add", "R99", "--quote", "поздняя", "--note", "n"],
        cwd=repo, env=env, capture_output=True, text=True,
    )
    assert out.returncode == 1
    assert "прогон закрыт" in out.stderr
    assert Run(repo, run.slug).requirement("R99") is None


# --- некрасивые падения ---


@pytest.mark.skipif(
    os.geteuid() == 0,
    reason="от root биты прав не действуют — проверять нечего (Termux/proot ходит под root)",
)
@pytest.mark.parametrize("cmd", [["next"], ["status"], ["manifest"], ["handoff"], ["report"]])
def test_unreadable_state_gives_a_human_message(run: Run, repo: Path, cmd, capsys):  # noqa: F811
    import os

    was = Path.cwd()
    run.state_path.chmod(0o000)
    try:
        os.chdir(repo)
        code = main(cmd)
        err = capsys.readouterr().err
    finally:
        os.chdir(was)
        run.state_path.chmod(0o644)
    assert code == 1
    assert "Traceback" not in err
    assert "Чекан" in err


def test_failed_task_reverts_its_requirement(run: Run):  # noqa: F811
    """В манифесте владельцу писалось «закрыто» о провалённой работе."""
    prepared(run)
    proof.run_command(run, "true", task="01")
    proof.close_task(run, "01")
    assert run.requirement("R01")["status"] == "done"

    run.set_task_status("01", "failed")
    assert run.requirement("R01")["status"] == "in-task"
    assert run.task("01").get("closed_at") is None


def test_new_run_does_not_inherit_a_foreign_ledger(repo: Path):  # noqa: F811
    """Потеря состояния переиспользовала каталог — и чужие доказательства вместе с ним."""
    first = Run.create(repo, "Одна и та же задача", "auto", "T0")
    first.append_evidence({"ts": "2020-01-01T00:00:00+03:00", "task": "01",
                           "cmd": "чужой прогон", "exit": 0})
    first.state_path.unlink()

    second = Run.create(repo, "Одна и та же задача", "auto", "T0")
    assert second.slug != first.slug
    assert second.evidence() == []


def test_run_bookkeeping_does_not_block_closing(run: Run, repo: Path):  # noqa: F811
    """Каталог прогона меняется от каждой команды — он не должен ломать закрытие.

    После коммита `.chekan/` отпечаток расходился всегда, и задачу нельзя было
    закрыть вообще: собственная бухгалтерия инструмента блокировала работу.
    """
    prepared(run)
    subprocess.run(["git", "add", "-A"], cwd=repo, check=True)
    subprocess.run(["git", "commit", "-qm", "прогон в истории"], cwd=repo, check=True)

    proof.run_command(run, "true", task="01")   # пишет state.json и журнал
    run.add_requirement("R02", "ещё одно", note="n")  # снова трогает состояние
    ok, messages = proof.close_task(run, "01")
    assert ok, messages
