"""Бриф задачи (R05) и скиллы машины (R09).

Рычаги порчи, каждый обязан давать красное:

1. Пересказать спеку вместо дословного куска (например обрезать раздел или
   переписать его своими словами в `brief.build`) — покраснеет
   `test_spec_sections_are_taken_from_disk_word_for_word`.
2. Дать подсказку скиллов слепому приёмщику (дописать её в промпт G4
   `chekan/__main__.py`) — покраснеет `test_blind_acceptance_prompt_has_no_skills`.
3. Советовать скилл там, где ничего не подошло (снять порог в
   `skills.suggest`) — покраснеет `test_a_task_about_the_tool_itself_gets_no_skill`.
4. Брать файл памяти из поля состояния вместо диска — покраснеют
   `test_memory_file_is_found_on_disk` и `test_missing_memory_is_said_in_words`.
"""

from __future__ import annotations

import io
import subprocess
import sys
from contextlib import redirect_stdout
from pathlib import Path

import pytest

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

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

SPEC = """# Спецификация

## R01 — Учёт расхода

**R01.1** Закрытие задачи записывает: модель, время работы, число прогонов.
Критерий: запись переживает перезапуск; поля разделены.

## R05 — Бриф задачи

**R05.1** `chekan brief NN` собирает один файл: разделы спеки по требованиям задачи,
границы из `interfaces.md`, зону, команду проверки и ловушки среды.

**R05.3** Бриф собирается чтением с диска. Критерий: строки спеки совпадают дословно.

## R10–R14 — Панель

**R10.1** Главная показывает картину прогона.
"""

INTERFACES = """# Границы

`brief.build` возвращает текст, `brief.write` кладёт его на диск. Второго
разбора спеки нет.
"""

MEMORY = """# CLAUDE.md

Ловушка среды: системный python без pytest, гонять только `.venv/bin/python`.
"""


@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:
    run = Run.create(repo, "Тестовый прогон", "normal", "T1")
    run.add_requirement("R05", "бриф задачи")
    (run.dir / "spec.md").write_text(SPEC, encoding="utf-8")
    (run.dir / "interfaces.md").write_text(INTERFACES, encoding="utf-8")
    run.add_task(
        "06", "Сборка брифа задачи", ["R05"],
        zone="chekan/brief.py,tests/test_brief.py",
        verify=".venv/bin/python -m pytest tests/test_brief.py -q",
    )
    return run


# --- R05.1: пять частей на месте ---


def test_brief_has_all_five_parts(run: Run):
    text = brief.build(run, "06")
    for part in ("## Зона", "## Команда проверки", "## Требования из спеки",
                 "## Границы", "## Ловушки среды"):
        assert part in text, f"в брифе нет части «{part}»"


def test_brief_carries_zone_and_verify_command(run: Run):
    text = brief.build(run, "06")
    assert "chekan/brief.py" in text and "tests/test_brief.py" in text
    assert ".venv/bin/python -m pytest tests/test_brief.py -q" in text


def test_brief_is_one_file_on_disk(run: Run):
    path = brief.write(run, "06")
    assert path.is_file() and path.parent == run.dir / "tasks"
    assert path.read_text(encoding="utf-8") == brief.build(run, "06")


def test_unknown_task_is_refused(run: Run):
    with pytest.raises(StateError):
        brief.build(run, "99")


# --- R05.3: дословно, чтением с диска ---


def test_spec_sections_are_taken_from_disk_word_for_word(run: Run):
    spec_text = (run.dir / "spec.md").read_text(encoding="utf-8")
    chunks = brief.spec_sections(spec_text, ["R05"])
    assert chunks, "раздел спеки по требованию задачи не найден"
    for chunk in chunks:
        assert chunk in spec_text, "кусок спеки не совпал с диском знак в знак"
    text = brief.build(run, "06")
    for line in chunks[0].splitlines():
        assert line in text, f"строка спеки не дошла до брифа дословно: {line!r}"


def test_only_the_requirements_of_this_task_get_in(run: Run):
    text = brief.build(run, "06")
    assert "R05 — Бриф задачи" in text
    assert "R01 — Учёт расхода" not in text, "в бриф попал чужой раздел спеки"


def test_a_range_heading_covers_every_requirement_in_it(run: Run):
    spec_text = (run.dir / "spec.md").read_text(encoding="utf-8")
    for rid in ("R10", "R12", "R14"):
        chunks = brief.spec_sections(spec_text, [rid])
        assert chunks and "R10–R14 — Панель" in chunks[0], rid
    assert not brief.spec_sections(spec_text, ["R20"])


def test_interfaces_come_from_the_file(run: Run):
    text = brief.build(run, "06")
    assert "`brief.build` возвращает текст" in text


def test_missing_interfaces_is_said_in_words(run: Run):
    (run.dir / "interfaces.md").unlink()
    text = brief.build(run, "06")
    assert "## Границы" in text
    assert "нет" in text.split("## Границы", 1)[1].split("##", 1)[0]


# --- R05.2: файл памяти определяется, а не берётся из пустого поля ---


def test_memory_file_is_found_on_disk(run: Run):
    assert run.state.get("memory_file") is None, "поле состояния и должно быть пустым"
    (run.root / "CLAUDE.md").write_text(MEMORY, encoding="utf-8")
    assert brief.memory_file(run.root) == run.root / "CLAUDE.md"
    text = brief.build(run, "06")
    assert "Ловушка среды: системный python без pytest" in text


def test_agents_md_is_the_second_choice(run: Run):
    (run.root / "AGENTS.md").write_text("# AGENTS\n\nЛовушка: диск на 100%.\n", encoding="utf-8")
    assert brief.memory_file(run.root) == run.root / "AGENTS.md"
    assert "Ловушка: диск на 100%." in brief.build(run, "06")

    (run.root / "CLAUDE.md").write_text(MEMORY, encoding="utf-8")
    assert brief.memory_file(run.root) == run.root / "CLAUDE.md", "CLAUDE.md первее"


def test_missing_memory_is_said_in_words(run: Run):
    assert brief.memory_file(run.root) is None
    text = brief.build(run, "06")
    tail = text.split("## Ловушки среды", 1)[1]
    assert "CLAUDE.md" in tail and "AGENTS.md" in tail
    assert "не записывал" in tail, "пропажа файла памяти не названа словом"


# --- R09.1: список скиллов, ни одной многострочной строки ---


def test_catalog_is_read_from_canon():
    found = skills.catalog()
    assert found, "canon/skills пуст — читать нечего"
    names = {s.name for s in found}
    assert {"zemlya-report-e2e", "pdf-visual-qa"} <= names


def test_every_row_is_one_line_and_only_the_first_sentence():
    for skill in skills.catalog():
        assert "\n" not in skill.summary, f"описание {skill.name} осталось многострочным"
        assert skill.summary.count(". ") == 0, f"у {skill.name} больше одного предложения"
    for row in skills.render():
        assert "\n" not in row and len(row) <= 100, row


def test_missing_canon_directory_is_not_a_crash(tmp_path: Path):
    assert skills.catalog(tmp_path / "нет-такого") == []
    assert skills.suggest({"title": "что угодно", "zone": "app.py"}, []) == []


# --- R09.2: подсказка по зоне и названию, иначе тишина ---


def test_a_task_about_zemlya_gets_the_zemlya_checker():
    task = {"title": "Проверка участка и AI-отчёт", "zone": "zemlya/"}
    assert "zemlya-report-e2e" in [s.name for s in skills.suggest(task)]


@pytest.mark.parametrize("title,zone", [
    ("Хранилище состояния прогона", "chekan/store.py"),
    ("Учёт расхода: измеренное отдельно от сообщённого", "chekan/store.py,chekan/report.py"),
    ("Сборка брифа задачи и подсказка скиллов", "chekan/brief.py,chekan/skills.py"),
    ("Замок на зону при старте задачи", "chekan/zonelock.py,tests/test_zonelock.py"),
])
def test_a_task_about_the_tool_itself_gets_no_skill(title: str, zone: str):
    """Ни один скилл машины не делает работу по самому инструменту.

    Слово из имени файла («report», «store») совпадает с именем скилла, но
    совпадением по смыслу не является: молчание тут — правильный ответ.
    """
    found = skills.suggest({"title": title, "zone": zone})
    assert found == [], f"случайный совет там, где ничего не подошло: {[s.name for s in found]}"


def test_brief_writes_the_hint_and_stays_silent_without_one(run: Run):
    text = brief.build(run, "06")
    assert "## Скиллы машины" in text
    section = text.split("## Скиллы машины", 1)[1]
    for skill in skills.catalog():
        assert skill.name not in section, f"в бриф просочился неподходящий скилл {skill.name}"

    run.add_task("07", "Проверка участка и AI-отчёт", ["R05"], zone="zemlya/")
    assert "zemlya-report-e2e" in brief.build(run, "07")


def test_the_hint_is_short(run: Run):
    run.add_task("07", "Проверка участка и AI-отчёт", ["R05"], zone="zemlya/")
    tail = brief.build(run, "07").split("## Скиллы машины", 1)[1]
    assert len([ln for ln in tail.splitlines() if ln.startswith("- `")]) <= skills.LIMIT


# --- R09.3: слепому приёмщику скиллов не дают ---


def test_blind_acceptance_prompt_has_no_skills(run: Run, monkeypatch):
    monkeypatch.chdir(run.root)
    buf = io.StringIO()
    with redirect_stdout(buf):
        assert main(["prompt", "g4"]) == 0
    printed = buf.getvalue()
    assert printed.strip(), "промпт слепого приёмщика пуст"
    for skill in skills.catalog():
        assert skill.name not in printed, f"слепой приёмщик увидел скилл {skill.name}"
    # Слова владельца из брифа в промпт входят как есть и скиллом называться могут:
    # проверяем текст самого инструмента, а не заказ.
    tool_text = printed.replace(run.state["title"], "")
    assert "скилл" not in tool_text.lower(), "слепому приёмщику подсказали скиллом"


# --- команды CLI ---


def test_cli_brief_and_skills(run: Run, monkeypatch):
    monkeypatch.chdir(run.root)
    buf = io.StringIO()
    with redirect_stdout(buf):
        assert main(["brief", "06"]) == 0
    assert (run.dir / "tasks" / "06-brief.md").is_file()
    assert "06-brief.md" in buf.getvalue()

    buf = io.StringIO()
    with redirect_stdout(buf):
        assert main(["skills"]) == 0
    printed = buf.getvalue()
    assert "zemlya-report-e2e" in printed
    for row in printed.splitlines():
        assert len(row) <= 120, row


def test_cli_brief_refuses_an_unknown_task(run: Run, monkeypatch):
    monkeypatch.chdir(run.root)
    assert main(["brief", "99"]) == 1
