"""Разговорный режим CLI: удобство человеку и полная тишина трубе.

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

1. Снять проверку `is_interactive` в `chekan/__main__.py` — режим откроется трубе, и
   `test_pipe_gets_no_shell_at_all` / `test_piped_status_is_byte_identical` покраснеют.
2. Убрать обработку неизвестной команды в `shell.dispatch` (пусть падает или
   выходит) — покраснеет `test_typo_survives_the_mode`.
3. Сделать любую строку `shell.LOGO` длиннее 80 знаков — покраснеет
   `test_logo_never_exceeds_eighty_columns`.
4. Выкинуть короткую форму из `shell.ALIASES` или её разбор — покраснеют
   `test_short_form_equals_full_form` и `test_short_form_runs_the_same_command`.
5. Убрать `ц` из `install.py` — покраснеет `test_install_puts_short_name_next_to_ceh`.
"""

from __future__ import annotations

import io
import os
import subprocess
import sys
from pathlib import Path

import pytest

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

import install  # noqa: E402
from chekan import shell  # noqa: E402
from chekan.__main__ import build_parser, main  # noqa: E402
from chekan.store import Run  # noqa: E402


@pytest.fixture(autouse=True)
def russian_by_default(monkeypatch):
    """Проверки ждут русских строк, а язык живёт в личной настройке владельца.

    Без этого выбор английского в `~/.config/chekan/config.json` красит чужие тесты
    красным — проверка начинает зависеть от того, что человек нажал однажды в терминале.
    """
    monkeypatch.setenv("CHEKAN_LANG", "ru")


@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)
    Run.create(tmp_path, "Тестовый прогон", "normal", "T1")
    return tmp_path


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


def _shell(repo: Path, *lines: str) -> int:
    cwd = Path.cwd()
    try:
        os.chdir(repo)
        return shell.start(lines=list(lines))
    finally:
        os.chdir(cwd)


def _run_cli(repo: Path, *args: str, stdin) -> subprocess.CompletedProcess:
    env = dict(os.environ)
    env["PYTHONPATH"] = str(ROOT) + (
        os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else ""
    )
    kwargs: dict = {"stdin": stdin} if stdin is not subprocess.PIPE else {"input": ""}
    return subprocess.run(
        [sys.executable, "-m", "chekan", *args],
        cwd=repo, env=env, text=True, capture_output=True, **kwargs,
    )


class _Tty(io.StringIO):
    def isatty(self) -> bool:  # noqa: D401
        return True


# --- главное: труба не видит режима ---


def test_pipe_gets_no_shell_at_all(repo: Path):
    """Вход не терминал — старое поведение до последнего знака: справка argparse.

    От этого живут `/api/cli` панели, хуки и чужие агенты: одна лишняя строка в
    их разборе — и они видят мусор вместо ответа.
    """
    res = _run_cli(repo, stdin=subprocess.PIPE)
    assert res.returncode == 2, res.stderr
    assert "usage" in res.stderr
    assert "КОДОЦЕХ" not in res.stdout + res.stderr
    assert shell.PROMPT not in res.stdout


def test_piped_status_is_byte_identical(repo: Path):
    """`echo "" | chekan status` — ровно то же, что без трубы вовсе."""
    piped = _run_cli(repo, "status", stdin=subprocess.PIPE)
    closed = _run_cli(repo, "status", stdin=subprocess.DEVNULL)
    assert piped.returncode == 0 == closed.returncode, piped.stderr
    assert piped.stdout == closed.stdout
    assert "Тестовый прогон" in piped.stdout
    assert "КОДОЦЕХ" not in piped.stdout
    assert shell.PROMPT not in piped.stdout


def test_interactivity_needs_both_ends(repo: Path):
    assert shell.is_interactive(_Tty(), _Tty())
    assert not shell.is_interactive(io.StringIO(), _Tty())
    assert not shell.is_interactive(_Tty(), io.StringIO())
    assert not shell.is_interactive(io.StringIO(), io.StringIO())


# --- режим ---


def test_mode_starts_with_the_kodoceh_logo(repo: Path, capsys):
    _shell(repo, "выход")
    printed = capsys.readouterr().out
    assert "К О Д О Ц Е Х" in printed
    for line in shell.LOGO:
        assert line in printed


def test_logo_never_exceeds_eighty_columns():
    """Узкий терминал не должен разъезжаться — ни одной строки шире 80."""
    for line in shell.LOGO:
        assert len(line) <= 80, line


def test_whole_start_screen_fits_eighty_columns(repo: Path):
    for line in shell.screen():
        assert len(line) <= 80, line


def test_every_command_is_listed_with_one_line_of_explanation(repo: Path):
    sub = shell._subparsers(build_parser())
    screen = shell.screen()
    helps = {a.dest: a.help for a in sub._choices_actions}
    for name in sub.choices:
        row = [line for line in screen if name in line.split()[:2]]
        assert row, f"команды {name} нет в списке режима"
        assert helps[name][:20] in row[0], f"у {name} нет пояснения в списке"


def test_aliases_are_unique_and_point_at_real_commands():
    sub = shell._subparsers(build_parser())
    assert len(set(shell.CYRILLIC_ALIASES.values())) == len(shell.CYRILLIC_ALIASES), "две кириллические короткие формы на одну команду"
    assert len(set(shell.LATIN_ALIASES.values())) == len(shell.LATIN_ALIASES), "две латинские короткие формы на одну команду"
    for short, full in shell.ALIASES.items():
        assert full in sub.choices, f"короткая форма {short} указывает в никуда: {full}"


def test_short_form_equals_full_form():
    assert shell.expand(["с"]) == ["status"]
    assert shell.expand(["s"]) == ["status"]
    assert shell.expand(["поч"]) == ["why"]
    assert shell.expand(["w"]) == ["why"]
    assert shell.expand(["бр"]) == ["brief"]
    assert shell.expand(["br"]) == ["brief"]
    assert shell.expand(["ск"]) == ["skills"]
    assert shell.expand(["sk"]) == ["skills"]
    assert shell.expand(["status"]) == ["status"]
    assert shell.expand(["з", "list"]) == ["task", "list"]
    assert shell.expand(["t", "list"]) == ["task", "list"]
    assert shell.expand(["нетакой"]) is None


def test_short_form_runs_the_same_command(repo: Path, capsys):
    _shell(repo, "с", "выход")
    short = capsys.readouterr().out
    _cli(repo, "status")
    full = capsys.readouterr().out
    assert full.strip(), "полная форма ничего не напечатала — сравнивать нечего"
    assert full.strip() in short


def test_typo_survives_the_mode(repo: Path, capsys):
    """Опечатка подсказывает ближайшую команду и не роняет режим."""
    code = _shell(repo, "stats", "с", "выход")
    printed = capsys.readouterr().out
    assert code == 0
    assert "stats" in printed and "status" in printed
    assert "Тестовый прогон" in printed, "после опечатки режим не выполнил следующую команду"


def test_broken_arguments_do_not_kill_the_mode(repo: Path, capsys):
    """argparse на плохих аргументах зовёт sys.exit — режим обязан это пережить."""
    code = _shell(repo, "task", "с", "выход")
    printed = capsys.readouterr().out
    assert code == 0
    assert "Тестовый прогон" in printed


def test_help_word_reprints_the_list(repo: Path, capsys):
    _shell(repo, "?", "выход")
    printed = capsys.readouterr().out
    assert printed.count("краткая сводка") >= 2


def test_dispatch_reports_command_exit_code(repo: Path, capsys):
    assert shell.dispatch("выход", print) is None
    assert shell.dispatch("", print) == 0
    assert shell.dispatch("нетакой", print) == 1


# --- короткое имя команды и установщик ---


def test_install_puts_short_name_next_to_chekan(tmp_path: Path, monkeypatch, capsys):
    monkeypatch.setattr(install, "bin_dir", lambda: tmp_path / "bin")
    assert install.install(dry=False) == 0
    capsys.readouterr()
    chekan_bin, short = install.shim_path(), install.short_path()
    assert short.name == "ч"
    assert chekan_bin.name == "chekan"
    assert chekan_bin.is_file() and short.is_file()
    assert chekan_bin.read_text(encoding="utf-8") == short.read_text(encoding="utf-8")
    assert os.access(short, os.X_OK), "короткое имя не исполняемое"


def test_uninstall_removes_old_and_new_shims(tmp_path: Path, monkeypatch, capsys):
    bin_d = tmp_path / "bin"
    monkeypatch.setattr(install, "bin_dir", lambda: bin_d)
    bin_d.mkdir(parents=True)
    # Создаём старые и новые шимы
    for name in ("chekan", "ч", "ceh", "ц", "ceh.bak-1", "chekan.bak-1"):
        (bin_d / name).write_text("dummy", encoding="utf-8")

    install.uninstall(dry=False)
    out = capsys.readouterr().out
    assert not (bin_d / "chekan").exists()
    assert not (bin_d / "ч").exists()
    assert not (bin_d / "ceh").exists()
    assert not (bin_d / "ц").exists()
    assert not (bin_d / "ceh.bak-1").exists()
    assert not (bin_d / "chekan.bak-1").exists()


def test_install_migrates_old_config_safely(tmp_path: Path, monkeypatch, capsys):
    old_cfg = tmp_path / ("auto" + "ceh")
    new_cfg = tmp_path / "chekan"
    monkeypatch.setattr(install, "old_config_dir", lambda: old_cfg)
    monkeypatch.setattr(install, "config_dir", lambda: new_cfg)
    monkeypatch.setattr(install, "bin_dir", lambda: tmp_path / "bin")

    old_cfg.mkdir(parents=True)
    (old_cfg / "telegram.json").write_text('{"token": "secret_token"}', encoding="utf-8")
    os.chmod(old_cfg / "telegram.json", 0o600)
    (old_cfg / "config.json").write_text('{"lang": "ru"}', encoding="utf-8")

    # Установка запускает миграцию
    assert install.install(dry=False) == 0
    out = capsys.readouterr().out

    # Проверяем, что секрет не утёк в stdout/stderr
    assert "secret_token" not in out
    assert new_cfg.is_dir()
    assert (new_cfg / "telegram.json").is_file()
    assert (new_cfg / "config.json").is_file()
    assert (new_cfg / "telegram.json").read_text(encoding="utf-8") == '{"token": "secret_token"}'

    # Проверяем права 0600
    mode = oct((new_cfg / "telegram.json").stat().st_mode & 0o777)
    assert mode in ("0o600", "0600")
