"""Повторное использование доказательства.

Пятнадцатиминутная проверка не должна гоняться трижды по коду, которого правка не
касалась. Но зачёт по отпечатку — самый удобный способ провести мимо приёмки код,
который никто не прогонял, поэтому каждый запрет здесь проверяется отдельно.
"""

from __future__ import annotations

import json
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path

import pytest

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

from chekan import gates, proof  # noqa: E402
from chekan.store import Run  # noqa: E402

HIT = "sh -c 'printf x >> hits.txt'"


@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 / "src").mkdir()
    (tmp_path / "src" / "app.py").write_text("print('ok')\n", encoding="utf-8")
    (tmp_path / "other").mkdir()
    (tmp_path / "other" / "side.py").write_text("side = 1\n", encoding="utf-8")
    (tmp_path / "scripts").mkdir()
    (tmp_path / "scripts" / "check.sh").write_text("printf x >> hits.txt\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("R01", "цитата", note="причина")
    r.add_task("01", "Таск в зоне", ["R01"], zone="src", verify=HIT)
    return r


def hits(repo: Path) -> int:
    f = repo / "hits.txt"
    return len(f.read_text(encoding="utf-8")) if f.is_file() else 0


def age_journal(run: Run, seconds: int) -> None:
    """Состарить записи журнала: суток ждать в тесте нечем."""
    ts = (datetime.now().astimezone() - timedelta(seconds=seconds)).isoformat(timespec="seconds")
    rows = [json.loads(ln) for ln in run.evidence_path.read_text(encoding="utf-8").splitlines() if ln.strip()]
    for row in rows:
        row["ts"] = ts
    run.evidence_path.write_text(
        "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in rows), encoding="utf-8"
    )


# --- зачёт без запуска ---


def test_same_code_same_command_is_reused(run: Run, repo: Path):
    first = proof.run_command(run, HIT, task="01")
    second = proof.run_command(run, HIT, task="01")
    assert hits(repo) == 1, "второй вызов запустил команду, хотя код не менялся"
    assert first.get("reused") is False
    assert second.get("reused") is True
    assert second["exit"] == 0
    assert second["reused_age_s"] >= 0
    assert second["reused_from"] == first["ts"]


def test_edit_inside_the_zone_forces_a_real_run(run: Run, repo: Path):
    proof.run_command(run, HIT, task="01")
    (repo / "src" / "app.py").write_text("print('changed')\n", encoding="utf-8")
    second = proof.run_command(run, HIT, task="01")
    assert second.get("reused") is False
    assert hits(repo) == 2, "правка в зоне обязана ломать переиспользование"


def test_edit_outside_the_zone_breaks_reuse_if_worktree_changed(run: Run, repo: Path):
    proof.run_command(run, HIT, task="01")
    (repo / "other" / "side.py").write_text("side = 2\n", encoding="utf-8")
    second = proof.run_command(run, HIT, task="01")
    assert second.get("reused") is False, "любая незакоммиченная правка в рабочей копии ломает переиспользование"
    assert hits(repo) == 2


def test_blind_acceptor_untracked_helper_reproduction(repo: Path):
    """Находка слепого приёмщика дословно:

    Задача с зоной z.py, проверка python3 verify.py импортирует helper.py
    (неотслеживаемый). Прогон зелёный → ломаем helper.py (вне зоны, неотслеживаемый)
    → повторный chekan run обязан запустить проверку по-настоящему (код 1), а не
    переиспользовать; chekan task done отказывает.
    """
    (repo / "z.py").write_text("z = 1\n", encoding="utf-8")
    (repo / "helper.py").write_text("def check(): return 42\n", encoding="utf-8")
    (repo / "verify.py").write_text("import helper, sys; sys.exit(0 if helper.check() == 42 else 1)\n", encoding="utf-8")
    subprocess.run(["git", "add", "z.py", "verify.py"], cwd=repo, check=True)
    subprocess.run(["git", "commit", "-qm", "add task files"], cwd=repo, check=True)

    run = Run.create(repo, "Прогон z", "normal", "T1")
    run.add_requirement("R01", "цитата", note="причина")
    run.add_task("01", "Таск z", ["R01"], zone="z.py", verify="python3 verify.py")

    first = proof.run_command(run, "python3 verify.py", task="01")
    assert first["exit"] == 0
    assert first.get("reused") is False

    # Ломаем helper.py (вне зоны z.py, неотслеживаемый git)
    (repo / "helper.py").write_text("def check(): return 0\n", encoding="utf-8")

    second = proof.run_command(run, "python3 verify.py", task="01")
    assert second.get("reused") is False, "неотслеживаемый helper.py изменён — переиспользование запрещено"
    assert second["exit"] == 1, "проверка обязана реально запуститься и вернуть код 1"

    ok, msgs = proof.close_task(run, "01")
    assert not ok, "закрытие обязано отказать"


def test_directory_in_command_expands_files_into_fingerprint(repo: Path):
    """Каталог в команде (pytest tests/) разворачивает файлы в отпечаток."""
    (repo / "tests").mkdir(exist_ok=True)
    (repo / "tests" / "test_a.py").write_text("def test_ok(): pass\n", encoding="utf-8")
    subprocess.run(["git", "add", "tests"], cwd=repo, check=True)
    subprocess.run(["git", "commit", "-qm", "add tests"], cwd=repo, check=True)

    run = Run.create(repo, "Проверка каталога в команде", "normal", "T1")
    run.add_requirement("R01", "цитата", note="причина")
    run.add_task("01", "Таск с каталогом", ["R01"], zone="src", verify="python3 -m pytest tests")

    first = proof.run_command(run, "python3 -m pytest tests", task="01")
    assert first.get("reused") is False

    # Меняем файл внутри каталога tests/
    (repo / "tests" / "test_a.py").write_text("def test_changed(): pass\n", encoding="utf-8")
    second = proof.run_command(run, "python3 -m pytest tests", task="01")
    assert second.get("reused") is False, "правка внутри tests/ ломает переиспользование команды с каталогом"


def test_edit_of_the_file_the_command_runs_forces_a_real_run(run: Run, repo: Path):
    cmd = "sh scripts/check.sh"
    proof.run_command(run, cmd, task="01")
    (repo / "scripts" / "check.sh").write_text("printf xx >> hits.txt\n", encoding="utf-8")
    second = proof.run_command(run, cmd, task="01")
    assert second.get("reused") is False, "запускаемый файл входит в отпечаток, даже лежащий вне зоны"
    assert hits(repo) == 3


def test_another_command_is_not_passed_off_as_the_run_one(run: Run, repo: Path):
    proof.run_command(run, HIT, task="01")
    other = proof.run_command(run, "sh -c 'printf y >> hits.txt; exit 3'", task="01")
    assert other.get("reused") is False
    assert other["exit"] == 3, "чужая команда засчиталась зелёной по чужому отпечатку"


# --- четыре запрета ---


def test_task_without_a_zone_never_reuses(repo: Path):
    run = Run.create(repo, "Прогон без зоны", "normal", "T1")
    run.add_requirement("R01", "цитата", note="причина")
    run.add_task("01", "Таск без зоны", ["R01"], zone="", verify=HIT)
    first = proof.run_command(run, HIT, task="01")
    second = proof.run_command(run, HIT, task="01")
    assert first.get("fingerprint") is None
    assert second.get("reused") is False
    assert hits(repo) == 2, "пустая зона — не «весь репозиторий»: переиспользования нет никогда"


def test_acceptance_runs_are_never_reused(run: Run, repo: Path):
    proof.run_command(run, HIT, task="01", kind="accept")
    second = proof.run_command(run, HIT, task="01", kind="accept")
    assert second.get("reused") is False
    assert hits(repo) == 2


def test_reuse_expires_after_a_day(run: Run, repo: Path):
    proof.run_command(run, HIT, task="01")
    age_journal(run, 25 * 3600)
    second = proof.run_command(run, HIT, task="01")
    assert second.get("reused") is False
    assert hits(repo) == 2


def test_reuse_does_not_cross_commits(run: Run, repo: Path):
    proof.run_command(run, HIT, task="01")
    subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "шаг"], cwd=repo, check=True)
    second = proof.run_command(run, HIT, task="01")
    assert second.get("reused") is False
    assert hits(repo) == 2


# --- приёмка и грязное дерево ---


def test_g4_reddens_on_a_reused_proof(run: Run, repo: Path):
    proof.run_command(run, HIT, task="01")
    proof.run_command(run, HIT, task="01")
    ok, _ = proof.close_task(run, "01")
    assert ok
    problems = gates.g4(run)
    assert any("переиспользован" in p for p in problems), "G4 засчитал доказательство без прогона"
    proof.run_command(run, HIT, task="01", reuse=False)
    assert not any("переиспользован" in p for p in gates.g4(run))


def test_reused_row_carries_the_tree_of_the_real_run(run: Run, repo: Path):
    first = proof.run_command(run, HIT, task="01")
    second = proof.run_command(run, HIT, task="01")
    assert second["reused"] is True
    assert second["tree"] == first["tree"]
    assert second["git_sha"] == first["git_sha"]
