"""Пауза прогона.

Смысл паузы — экономить квоту: владелец останавливает работу с панели и
продолжает позже. Две вещи, которые здесь легко сделать неправильно и которые
поэтому закрыты тестами:

1. Пауза обязана **запрещать новое**, а не притворяться. Если `chekan task start`
   на паузе проходит, кнопка декоративная.
2. Время простоя обязано **выпадать из длительностей**. Иначе медиана задачи и
   оценка «осталось» врут ровно на время, когда никто не работал, — а владелец
   принимает решения именно по ним.
"""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path

import pytest

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

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


@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:
    r = Run.create(repo, "Тестовый прогон", "normal", "T1")
    r.add_requirement("R01", "цитата", note="причина")
    r.add_task("01", "задача", ["R01"], verify="true")
    return r


# --- состояние ---


def test_pause_and_resume_are_recorded_as_spans(run: Run):
    assert not run.is_paused()
    assert run.pause() is True
    assert run.is_paused()
    assert run.paused_since()
    assert run.resume() is True
    assert not run.is_paused()
    spans = run.state["pauses"]
    assert len(spans) == 1 and spans[0]["to"], "отрезок простоя не закрыт"


def test_pause_is_idempotent(run: Run):
    """Двойное нажатие кнопки не должно плодить отрезки — иначе счёт простоя поедет."""
    assert run.pause() is True
    assert run.pause() is False
    assert len(run.state["pauses"]) == 1
    assert run.resume() is True
    assert run.resume() is False


def test_two_open_pauses_are_rejected_by_validation():
    state = Run.__new__(Run)  # состояние проверяется отдельно от объекта
    del state
    from chekan.store import new_state

    st = new_state("s", "t", "normal", "T1")
    st["pauses"] = [{"from": "2026-01-01T00:00:00+00:00", "to": None},
                    {"from": "2026-01-01T01:00:00+00:00", "to": None}]
    with pytest.raises(StateError):
        validate(st)


# --- длительности ---


def _st(spans: list[dict]) -> dict:
    return {"pauses": spans}


def test_pause_outside_the_task_does_not_shorten_it():
    st = _st([{"from": "2026-01-01T10:00:00+00:00", "to": "2026-01-01T11:00:00+00:00"}])
    assert paused_ms_between(st, "2026-01-01T12:00:00+00:00", "2026-01-01T13:00:00+00:00") == 0


def test_pause_inside_the_task_is_subtracted_whole():
    st = _st([{"from": "2026-01-01T12:10:00+00:00", "to": "2026-01-01T12:40:00+00:00"}])
    got = paused_ms_between(st, "2026-01-01T12:00:00+00:00", "2026-01-01T13:00:00+00:00")
    assert got == 30 * 60 * 1000


def test_partial_overlap_counts_only_the_overlap():
    """Пауза началась до задачи и кончилась в её середине."""
    st = _st([{"from": "2026-01-01T11:30:00+00:00", "to": "2026-01-01T12:15:00+00:00"}])
    got = paused_ms_between(st, "2026-01-01T12:00:00+00:00", "2026-01-01T13:00:00+00:00")
    assert got == 15 * 60 * 1000


def test_several_pauses_add_up():
    st = _st([
        {"from": "2026-01-01T12:05:00+00:00", "to": "2026-01-01T12:15:00+00:00"},
        {"from": "2026-01-01T12:30:00+00:00", "to": "2026-01-01T12:35:00+00:00"},
    ])
    got = paused_ms_between(st, "2026-01-01T12:00:00+00:00", "2026-01-01T13:00:00+00:00")
    assert got == 15 * 60 * 1000


def test_open_pause_counts_up_to_now(run: Run):
    run.pause()
    got = paused_ms_between(run.state, run.state["started_at"], None)
    assert got >= 0, "незакрытая пауза обязана считаться до сих пор, а не пропадать"


def test_run_without_pauses_key_survives(run: Run):
    """Прогоны, заведённые до появления пауз, обязаны читаться и считаться."""
    st = dict(run.state)
    st.pop("pauses", None)
    assert paused_ms_between(st, "2026-01-01T12:00:00+00:00", "2026-01-01T13:00:00+00:00") == 0


# --- отказы CLI ---


def _cli(repo: Path, *args: str) -> int:
    cwd = Path.cwd()
    try:
        import os

        os.chdir(repo)
        return main(list(args))
    finally:
        import os

        os.chdir(cwd)


def test_task_start_is_refused_while_paused(run: Run, repo: Path, capsys):
    run.pause()
    assert _cli(repo, "task", "start", "01") == 1
    assert "на паузе" in capsys.readouterr().err
    assert Run.load(repo).task("01")["status"] == "pending", "задача всё-таки взята в работу"


def test_advance_is_refused_while_paused(run: Run, repo: Path, capsys):
    run.pause()
    assert _cli(repo, "advance") == 1
    assert "на паузе" in capsys.readouterr().err


def test_task_start_works_again_after_resume(run: Run, repo: Path):
    run.pause()
    _cli(repo, "task", "start", "01")
    run.resume()
    assert _cli(repo, "task", "start", "01") == 0
    assert Run.load(repo).task("01")["status"] == "in-progress"


def test_next_says_it_is_paused_first(run: Run, repo: Path, capsys):
    run.pause()
    assert _cli(repo, "next") == 0
    first = capsys.readouterr().out.splitlines()[0]
    assert "ПАУЗЕ" in first, "о паузе должно быть сказано первой строкой, а не в середине"


# --- сервер ---


def test_command_endpoint_refuses_a_foreign_address():
    """Привязка к петле защищает от сети, проверка адреса — от забытого проброса порта."""
    from chekan.serve_app import LOCAL, Handler

    assert "127.0.0.1" in LOCAL
    assert "10.0.0.5" not in LOCAL
    assert hasattr(Handler, "do_POST")


# --- остановка прогона отличается от работы ---


def test_quiet_run_is_reported_as_stalled(run: Run, monkeypatch):
    """Исполнитель умер — задача так и висит «идёт», а панель бодро рисует кольцо.

    Отличить работу от тишины можно только по движению: любое действие пишет
    в состояние или в журнал доказательств. Причину знать неоткуда, поэтому
    инструмент говорит ровно то, что видит.
    """
    from chekan import board

    run.set_task_status("01", "in-progress")
    assert board._silence(run) is None, "свежий прогон не может считаться замершим"

    # отматываем часы вперёд на час — движения за это время не было
    import datetime as dt

    real = dt.datetime

    class Later(dt.datetime):
        @classmethod
        def now(cls, tz=None):
            return real.now(tz) + dt.timedelta(hours=1)

    monkeypatch.setattr(board, "datetime", Later, raising=False)
    monkeypatch.setattr("datetime.datetime", Later)
    quiet = board._silence(run)
    assert quiet is not None and quiet > 15 * 60 * 1000


def test_pause_is_not_a_stall(run: Run, monkeypatch):
    """На паузе тишина законна: это решение владельца, а не смерть исполнителя."""
    from chekan import board

    run.set_task_status("01", "in-progress")
    run.pause()

    import datetime as dt

    real = dt.datetime

    class Later(dt.datetime):
        @classmethod
        def now(cls, tz=None):
            return real.now(tz) + dt.timedelta(hours=1)

    monkeypatch.setattr("datetime.datetime", Later)
    assert board._silence(run) is None
