"""Язык интерфейса: русский по умолчанию, английский настоящий, вопрос — один раз.

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

1. Убрать проверку `is_interactive` в `i18n.choose_on_first_run` — вопрос уйдёт в
   трубу, и покраснеют `test_pipe_is_never_asked_about_language` и
   `test_piped_cli_runs_without_a_question_and_without_config`.
2. Перестать сохранять выбор (`save_language`) или перестать читать сохранённое —
   покраснеют `test_first_run_asks_once_and_remembers` и
   `test_second_run_is_silent_in_a_real_terminal`.
3. Выкинуть любую строку из `i18n.CATALOG` (или добавить команду без перевода) —
   покраснеют `test_catalog_covers_every_command_and_its_explanation` и
   `test_english_terminal_session_has_no_cyrillic_in_tool_strings`.
4. Завести в каталоге перевод для слов владельца (заголовок прогона, бриф) —
   покраснеет `test_owner_words_are_never_translated`.
"""

from __future__ import annotations

import io
import json
import os
import pty
import re
import select
import subprocess
import sys
import time
from pathlib import Path

import pytest

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

from chekan import i18n, shell  # noqa: E402
from chekan.store import Run  # noqa: E402

CYRILLIC = re.compile(r"[А-Яа-яЁё]")

TITLE = "Тестовый прогон"
BRIEF = "Тестовый прогон"

# ЯВНЫЙ список того, что из проверки английского исключено, и почему. Молчаливых
# исключений здесь нет: всё, чего нет в этом списке, обязано быть по-английски.
EXEMPT_REASONS = {
    "логотип": "марка КодоЦех — имя, а не строка интерфейса",
    "приглашение": "«ц › » — имя команды, которой открывают режим",
    "короткие формы": "мнемоники команд (с, з, п) — тоже имена, не текст",
    "слова владельца": "заголовок прогона и бриф: переводить их — врать",
}


@pytest.fixture
def conf(tmp_path: Path, monkeypatch) -> Path:
    """Настройки в песочнице: боевой ~/.config/chekan не трогаем."""
    path = tmp_path / "config" / "config.json"
    monkeypatch.setenv(i18n.CONFIG_ENV, str(path))
    monkeypatch.delenv(i18n.LANG_ENV, raising=False)
    i18n.forget()
    yield path
    i18n.forget()


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


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


def _env(conf: Path, **extra: str) -> dict:
    env = dict(os.environ)
    env["PYTHONPATH"] = str(ROOT) + (
        os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else ""
    )
    env[i18n.CONFIG_ENV] = str(conf)
    env.pop(i18n.LANG_ENV, None)
    env.update(extra)
    return env


def _terminal_session(repo: Path, conf: Path, feed: str, timeout: float = 30.0) -> str:
    """Настоящий прогон CLI за терминалом: псевдотерминал на вход и на выход."""
    master, slave = pty.openpty()
    proc = subprocess.Popen(
        [sys.executable, "-m", "chekan"],
        cwd=repo, env=_env(conf),
        stdin=slave, stdout=slave, stderr=slave, close_fds=True,
    )
    os.close(slave)
    os.write(master, feed.encode("utf-8"))
    chunks: list[bytes] = []
    deadline = time.time() + timeout
    while time.time() < deadline:
        ready, _w, _x = select.select([master], [], [], 0.3)
        if ready:
            try:
                data = os.read(master, 65536)
            except OSError:
                break
            if not data:
                break
            chunks.append(data)
        elif proc.poll() is not None:
            break
    os.close(master)
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:  # вопрос повис — это тоже красное
        proc.kill()
        raise AssertionError("режим не завершился: похоже, ждёт ответа")
    return b"".join(chunks).decode("utf-8", "replace")


def _tool_lines(printed: str) -> list[str]:
    """Строки инструмента: вычитаем ровно то, что перечислено в EXEMPT_REASONS."""
    logo = set(shell.LOGO)
    rows: list[str] = []
    for raw in printed.splitlines():
        line = raw.replace("\r", "").rstrip()
        if not line.strip() or line in logo:
            continue
        line = line.replace(shell.PROMPT.strip(), " ")  # приглашение
        words = line.split()
        if words and words[0] in shell.ALIASES:  # столбец коротких форм
            words = words[1:]
        rest = " ".join(words)
        if rest:
            rows.append(rest)
    return rows


# --- по умолчанию русский ---


def test_default_language_is_russian(conf: Path):
    assert not conf.exists()
    assert i18n.language() == i18n.RUSSIAN
    assert i18n.translate("краткая сводка") == "краткая сводка"
    assert any("краткая сводка" in row for row in shell.screen())


def test_unknown_string_comes_back_russian_and_does_not_crash(conf: Path):
    weird = "строки такой в каталоге нет и не будет"
    assert i18n.translate(weird, i18n.ENGLISH) == weird
    assert i18n.translate("", i18n.ENGLISH) == ""


def test_broken_config_is_treated_as_no_choice(conf: Path):
    conf.parent.mkdir(parents=True, exist_ok=True)
    conf.write_text("{не json", encoding="utf-8")
    i18n.forget()
    assert i18n.stored_language() is None
    assert i18n.language() == i18n.RUSSIAN


# --- английский настоящий ---


def test_catalog_covers_every_command_and_its_explanation(conf: Path, monkeypatch):
    monkeypatch.setenv(i18n.LANG_ENV, i18n.ENGLISH)
    i18n.forget()
    missing = [c.full for c in shell.commands() if c.help and c.help not in i18n.CATALOG]
    assert not missing, f"нет перевода пояснения у команд: {missing} (добавьте в i18n.CATALOG)"
    for command in shell.commands():
        english = i18n.translate(command.help)
        assert not CYRILLIC.search(english), f"перевод {command.full} остался русским: {english}"


def test_placeholders_survive_translation(conf: Path):
    assert "{cmd}" in i18n.translate("Нет такой команды: {cmd}", i18n.ENGLISH)
    assert "{near}" in i18n.translate("Может быть, «{near}»? Весь список: «?»", i18n.ENGLISH)
    for source, english in i18n.CATALOG.items():
        assert set(re.findall(r"{(\w+)}", source)) == set(re.findall(r"{(\w+)}", english)), source


def test_english_screen_has_no_cyrillic_in_tool_strings(conf: Path, monkeypatch):
    monkeypatch.setenv(i18n.LANG_ENV, i18n.ENGLISH)
    i18n.forget()
    for row in _tool_lines("\n".join(shell.screen())):
        assert not CYRILLIC.search(row), f"английский протёк кириллицей: {row}"


def test_english_terminal_session_has_no_cyrillic_in_tool_strings(repo: Path, conf: Path):
    """Настоящий прогон CLI за терминалом, язык английский, вопрос уже отвечен."""
    i18n.save_language(i18n.ENGLISH)
    printed = _terminal_session(repo, conf, "?\nexit\n")
    assert "short summary" in printed, "список команд не по-английски"
    for row in _tool_lines(printed):
        assert not CYRILLIC.search(row), f"английский протёк кириллицей: {row}"


def test_unknown_command_answers_in_english(repo: Path, conf: Path):
    i18n.save_language(i18n.ENGLISH)
    printed = _terminal_session(repo, conf, "stats\nexit\n")
    assert "No such command" in printed
    for row in _tool_lines(printed):
        assert not CYRILLIC.search(row), row


def test_status_and_board_output_in_english(repo: Path, conf: Path):
    """При CHEKAN_LANG=en статус и доска выводят английские строки интерфейса."""
    i18n.save_language(i18n.ENGLISH)
    res_status = subprocess.run(
        [sys.executable, "-m", "chekan", "status"],
        cwd=repo, env=_env(conf, CHEKAN_LANG=i18n.ENGLISH), text=True, capture_output=True,
    )
    assert res_status.returncode == 0
    assert "Phase:" in res_status.stdout
    assert "Mode:" in res_status.stdout
    assert "Scale:" in res_status.stdout

    res_board = subprocess.run(
        [sys.executable, "-m", "chekan", "board"],
        cwd=repo, env=_env(conf, CHEKAN_LANG=i18n.ENGLISH), text=True, capture_output=True,
    )
    assert res_board.returncode == 0
    assert "TASKS" in res_board.stdout
    assert "PHASES" in res_board.stdout
    assert "GATES" in res_board.stdout


def test_short_forms_work_in_both_cyrillic_and_latin_layouts(repo: Path, conf: Path):
    """Короткие формы работают на обеих раскладках всегда."""
    i18n.save_language(i18n.ENGLISH)
    # Латинские короткие формы
    printed_lat = _terminal_session(repo, conf, "s\nexit\n")
    assert TITLE in printed_lat
    # Кириллические короткие формы даже в английском режиме
    printed_cyr = _terminal_session(repo, conf, "с\nexit\n")
    assert TITLE in printed_cyr


# --- слова владельца ---


def test_owner_words_are_never_translated(repo: Path, conf: Path):
    """Заголовок прогона и бриф — слова владельца: в каталоге их нет, вывод дословен."""
    assert TITLE not in i18n.CATALOG and BRIEF not in i18n.CATALOG
    assert i18n.translate(TITLE, i18n.ENGLISH) == TITLE
    i18n.save_language(i18n.ENGLISH)
    printed = _terminal_session(repo, conf, "status\nexit\n")
    assert TITLE in printed, "заголовок прогона не дошёл до вывода дословно"


# --- вопрос задаётся один раз и только человеку ---


def test_first_run_asks_once_and_remembers(conf: Path):
    asked: list[str] = []

    def ask(question: str) -> str:
        asked.append(question)
        return "2"

    code = i18n.choose_on_first_run(write=lambda _t: None, ask=ask, stdin=_Tty(), stdout=_Tty())
    assert code == i18n.ENGLISH
    assert len(asked) == 1
    assert json.loads(conf.read_text(encoding="utf-8"))[i18n.CONFIG_KEY] == i18n.ENGLISH

    def never(_question: str) -> str:
        raise AssertionError("язык спрошен второй раз")

    i18n.forget()
    assert i18n.choose_on_first_run(
        write=lambda _t: None, ask=never, stdin=_Tty(), stdout=_Tty()
    ) == i18n.ENGLISH


def test_pipe_is_never_asked_about_language(conf: Path):
    def never(_question: str) -> str:
        raise AssertionError("вопрос ушёл в трубу — панель и хуки повиснут")

    for stdin, stdout in ((io.StringIO(), _Tty()), (_Tty(), io.StringIO()), (io.StringIO(), io.StringIO())):
        assert i18n.choose_on_first_run(
            write=lambda _t: None, ask=never, stdin=stdin, stdout=stdout
        ) == i18n.RUSSIAN
    assert not conf.exists(), "неинтерактивный вход не смеет записывать выбор"


def test_piped_cli_runs_without_a_question_and_without_config(repo: Path, conf: Path):
    res = subprocess.run(
        [sys.executable, "-m", "chekan", "status"],
        cwd=repo, env=_env(conf), text=True, capture_output=True,
        stdin=subprocess.DEVNULL, timeout=60,
    )
    assert res.returncode == 0, res.stderr
    assert "Interface language" not in res.stdout + res.stderr
    assert "Язык интерфейса" not in res.stdout + res.stderr
    assert TITLE in res.stdout
    assert not conf.exists(), "труба не выбирает язык и не пишет настройки"


def test_real_terminal_asks_then_second_run_is_silent(repo: Path, conf: Path):
    first = _terminal_session(repo, conf, "2\nexit\n")
    assert "Interface language" in first, "человека за терминалом не спросили о языке"
    assert json.loads(conf.read_text(encoding="utf-8"))[i18n.CONFIG_KEY] == i18n.ENGLISH

    second = _terminal_session(repo, conf, "exit\n")
    assert "Interface language" not in second, "выбор языка спрошен второй раз"
    assert "short summary" in second, "сохранённый английский не применился"


def test_default_answer_is_russian(conf: Path):
    code = i18n.choose_on_first_run(
        write=lambda _t: None, ask=lambda _q: "", stdin=_Tty(), stdout=_Tty()
    )
    assert code == i18n.RUSSIAN
    assert json.loads(conf.read_text(encoding="utf-8"))[i18n.CONFIG_KEY] == i18n.RUSSIAN


def test_environment_beats_the_saved_choice_and_skips_the_question(conf: Path, monkeypatch):
    i18n.save_language(i18n.RUSSIAN)
    monkeypatch.setenv(i18n.LANG_ENV, i18n.ENGLISH)
    i18n.forget()
    assert i18n.language() == i18n.ENGLISH

    def never(_question: str) -> str:
        raise AssertionError("язык задан окружением — спрашивать нечего")

    assert i18n.choose_on_first_run(
        write=lambda _t: None, ask=never, stdin=_Tty(), stdout=_Tty()
    ) == i18n.ENGLISH


def test_saving_language_keeps_neighbouring_settings(conf: Path):
    conf.parent.mkdir(parents=True, exist_ok=True)
    conf.write_text(json.dumps({"fixed_port": 0}), encoding="utf-8")
    i18n.forget()
    i18n.save_language(i18n.ENGLISH)
    data = json.loads(conf.read_text(encoding="utf-8"))
    assert data["fixed_port"] == 0, "запись языка затёрла чужую настройку"
    assert data[i18n.CONFIG_KEY] == i18n.ENGLISH


def test_exemptions_are_named_not_silent():
    """Список исключений из проверки английского — явный и с причиной у каждого."""
    assert EXEMPT_REASONS and all(reason for reason in EXEMPT_REASONS.values())
