"""feat/telegram-live-tracker: tests for the live Telegram task tracker.

Covers (per DEV_TASK_TELEGRAM_TRACKER.md):
  * short_model_name: provider/claude- prefix trimming.
  * render_task_tracker: per-stage line format (in↓/out↑, model, cost, minutes),
    the "✅/⏸️ Подтверждение BRD · твоё время" line, the 💰 totals, and the finish block
    (⏱️ three times + 🔗/📦).
  * first message -> sendMessage stores message_id; transition -> editMessageText.
  * fallback: editMessageText fails -> a NEW message is sent and the id updated.
  * which alerts go out SEPARATELY (approve-gate / deploy-fail / agent-fail /
    error) vs which do NOT (QG-pending / agent-start / stage-transition).

Isolated temp DB; no network (httpx is patched).
"""

import os
import tempfile

os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")

_test_db = os.path.join(tempfile.gettempdir(), "test_orchestrator_tracker.db")
os.environ["ORCH_DB_PATH"] = _test_db

from unittest.mock import MagicMock, patch  # noqa: E402

import pytest  # noqa: E402

import src.db as db_module  # noqa: E402
from src.db import init_db, get_db  # noqa: E402
from src import notifications as N  # noqa: E402
from src import usage as U  # noqa: E402


@pytest.fixture(autouse=True)
def setup_db(monkeypatch):
    monkeypatch.setattr(db_module.settings, "db_path", _test_db, raising=False)
    if os.path.exists(_test_db):
        os.unlink(_test_db)
    init_db()
    # Re-enable send_telegram (conftest stubs it to a no-op); these tests patch
    # httpx / the lower-level helpers explicitly per case.
    yield
    if os.path.exists(_test_db):
        os.unlink(_test_db)


# --------------------------------------------------------------------------- #
# helpers to build a task + runs in the DB
# --------------------------------------------------------------------------- #
def _mk_task(stage="development", title="\u0422\u0440\u0435\u043a\u0438 \u0441 \u0437\u0443\u043c\u0430 z5",
             wid="ET-012", brd_start=None, brd_end=None):
    conn = get_db()
    cur = conn.execute(
        "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, "
        "brd_review_started_at, brd_review_ended_at) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
        ("p1", wid, "enduro-trails", "feature/ET-012-x", stage, title,
         brd_start, brd_end),
    )
    tid = cur.lastrowid
    conn.commit()
    conn.close()
    return tid


def _mk_run(task_id, agent, started, finished, in_tok, out_tok,
            cache_read=0, cache_creation=0, cost=0.0, model=None, exit_code=0):
    conn = get_db()
    cur = conn.execute(
        "INSERT INTO agent_runs (task_id, agent, started_at, finished_at, "
        "exit_code, input_tokens, output_tokens, cache_read_tokens, "
        "cache_creation_tokens, cost_usd, model) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        (task_id, agent, started, finished, exit_code, in_tok, out_tok,
         cache_read, cache_creation, cost, model),
    )
    rid = cur.lastrowid
    conn.commit()
    conn.close()
    return rid


# --------------------------------------------------------------------------- #
# short_model_name
# --------------------------------------------------------------------------- #
def test_short_model_name():
    assert U.short_model_name("tokenator/claude-opus-4-8") == "opus-4-8"
    assert U.short_model_name("vibecode/claude-sonnet-4.6") == "sonnet-4.6"
    assert U.short_model_name("claude-opus-4-8") == "opus-4-8"
    assert U.short_model_name("opus-4-8") == "opus-4-8"
    assert U.short_model_name(None) == ""
    assert U.short_model_name("") == ""


def test_parse_usage_extracts_model_from_modelusage():
    blob = (
        '{"total_cost_usd":0.01,'
        '"usage":{"input_tokens":10,"output_tokens":5},'
        '"modelUsage":{"claude-opus-4-8":{"inputTokens":10,"outputTokens":5}}}'
    )
    u = U.parse_usage_from_text(blob)
    assert u["model"] == "claude-opus-4-8"


# --------------------------------------------------------------------------- #
# render_task_tracker
# --------------------------------------------------------------------------- #
def test_render_in_progress_stage_lines_and_totals():
    tid = _mk_task(stage="deploy", brd_start="2026-06-04 10:00:00",
                   brd_end="2026-06-04 10:08:00")
    # Analysis: 10м, 1.1M in (mostly cache) / 39.6k out, $2.38, opus-4-8
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=1000, out_tok=39600, cache_read=1_100_000, cost=2.38,
            model="tokenator/claude-opus-4-8")
    _mk_run(tid, "architect", "2026-06-04 10:08:00", "2026-06-04 10:17:00",
            in_tok=500, out_tok=34400, cache_read=1_500_000, cost=2.24,
            model="tokenator/claude-opus-4-8")
    _mk_run(tid, "developer", "2026-06-04 10:17:00", "2026-06-04 10:28:00",
            in_tok=400, out_tok=45800, cache_read=8_400_000, cost=7.29,
            model="tokenator/claude-opus-4-8")
    _mk_run(tid, "reviewer", "2026-06-04 10:28:00", "2026-06-04 10:31:00",
            in_tok=300, out_tok=12900, cache_read=1_200_000, cost=1.53,
            model="vibecode/claude-sonnet-4.6")
    _mk_run(tid, "tester", "2026-06-04 10:31:00", "2026-06-04 10:36:00",
            in_tok=200, out_tok=19500, cache_read=1_200_000, cost=1.51,
            model="vibecode/claude-sonnet-4.6")
    # deployer started but not finished -> active "идёт" line.
    _mk_run(tid, "deployer", "2026-06-04 10:36:00", None,
            in_tok=0, out_tok=0, model=None, exit_code=None)

    text = N.render_task_tracker(tid)

    # Header in-progress
    assert text.startswith("\U0001f6e0\ufe0f ET-012 \u00b7 \u0422\u0440\u0435\u043a\u0438")
    # Per-stage format: in↓/out↑ · cost · model
    assert "\u2705 Анализ" in text
    assert "10\u043c" in text          # analysis duration
    assert "39.6k\u2191" in text       # analysis out
    assert "$2.38" in text
    assert "opus-4-8" in text
    assert "sonnet-4.6" in text        # reviewer/tester model
    # BRD review line (human time, ended)
    assert "Подтверждение BRD" in text
    assert "\u0442\u0432\u043e\u0451 \u0432\u0440\u0435\u043c\u044f" in text
    # Active stage
    assert "\U0001f504 Внедрение" in text
    assert "\u0438\u0434\u0451\u0442" in text
    # Totals line present with 💰
    assert "\U0001f4b0" in text
    # In-progress: no final ⏱️ line
    assert "\u0412\u0441\u0435\u0433\u043e" not in text


def test_render_brd_review_waiting_shows_hourglass():
    tid = _mk_task(stage="analysis", brd_start="2026-06-04 10:00:00",
                   brd_end=None)
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=1000, out_tok=39600, cache_read=1_100_000, cost=2.38,
            model="tokenator/claude-opus-4-8")
    text = N.render_task_tracker(tid)
    assert "Подтверждение BRD" in text
    assert "\u23f3" in text  # hourglass while waiting


def test_render_done_has_times_and_links():
    tid = _mk_task(stage="done", brd_start="2026-06-04 10:00:00",
                   brd_end="2026-06-04 10:08:00")
    # set created/updated to compute wall clock
    conn = get_db()
    conn.execute(
        "UPDATE tasks SET created_at='2026-06-04 09:00:00', "
        "updated_at='2026-06-04 09:56:00' WHERE id=?", (tid,))
    conn.commit()
    conn.close()
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=1000, out_tok=39600, cache_read=1_100_000, cost=2.38,
            model="tokenator/claude-opus-4-8")
    _mk_run(tid, "deployer", "2026-06-04 09:50:00", "2026-06-04 09:56:00",
            in_tok=400, out_tok=22400, cache_read=1_600_000, cost=1.73,
            model="tokenator/claude-opus-4-8")

    with patch("src.notifications.httpx") as _hx:
        # No PR found -> just "📦 deployed"
        _resp = MagicMock(status_code=200)
        _resp.json.return_value = []
        _hx.get.return_value = _resp
        text = N.render_task_tracker(tid)

    assert text.startswith("\U0001f389 ET-012")
    assert "\u0413\u041e\u0422\u041e\u0412\u041e" in text
    # ⏱️ with three times
    assert "\u23f1\ufe0f" in text
    assert "\u0412\u0441\u0435\u0433\u043e" in text
    assert "\u0430\u0433\u0435\u043d\u0442\u044b" in text
    assert "\u0442\u0432\u043e\u0451" in text
    # 📦 deployed line
    assert "\U0001f4e6" in text


def test_render_escapes_html_in_title():
    tid = _mk_task(stage="analysis", title="A <b>& B</b>")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.0)
    text = N.render_task_tracker(tid)
    assert "&lt;b&gt;" in text
    assert "&amp;" in text


def test_render_omits_model_when_unknown():
    tid = _mk_task(stage="analysis")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.0, model=None)
    text = N.render_task_tracker(tid)
    # No trailing " · <model>" — line ends at cost.
    line = [l for l in text.splitlines() if l.startswith("\u2705 Анализ")][0]
    assert line.rstrip().endswith("$0.00")


# --------------------------------------------------------------------------- #
# tracker send / edit / fallback
# --------------------------------------------------------------------------- #
def test_first_call_sends_message_and_stores_id(monkeypatch):
    tid = _mk_task(stage="analysis")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", None, in_tok=0, out_tok=0,
            exit_code=None)

    sent = {}
    def _fake_send(text, disable_notification=False):
        sent["text"] = text
        sent["silent"] = disable_notification
        return 555
    monkeypatch.setattr(N, "send_telegram", _fake_send)
    monkeypatch.setattr(N, "edit_telegram", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not edit on first call")))

    N.update_task_tracker(tid)

    from src.db import get_tracker_message_id
    assert get_tracker_message_id(tid) == 555
    assert sent["silent"] is True  # tracker is silent


def test_second_call_edits_existing_message(monkeypatch):
    # ORCH-067: the default flipped to bump; this case asserts the edit-mode
    # contract, so pin edit mode explicitly.
    monkeypatch.setattr(N._get_settings(), "tracker_mode", "edit", raising=False)
    tid = _mk_task(stage="development")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    from src.db import set_tracker_message_id
    set_tracker_message_id(tid, 777)

    edited = {}
    monkeypatch.setattr(N, "edit_telegram",
                        lambda mid, text: edited.update(mid=mid) or N.EDIT_OK)
    monkeypatch.setattr(N, "send_telegram",
                        lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not send when edit succeeds")))

    N.update_task_tracker(tid)
    assert edited["mid"] == 777


def test_fallback_to_new_message_when_edit_gone(monkeypatch):
    """edit returns 'gone' (message deleted/too old) -> send NEW + update id."""
    tid = _mk_task(stage="development")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    from src.db import set_tracker_message_id, get_tracker_message_id
    set_tracker_message_id(tid, 100)

    monkeypatch.setattr(N, "edit_telegram", lambda mid, text: N.EDIT_GONE)
    monkeypatch.setattr(N, "send_telegram", lambda text, disable_notification=False: 200)

    N.update_task_tracker(tid)
    assert get_tracker_message_id(tid) == 200  # id updated to the new message


def test_not_modified_does_not_send_new_message(monkeypatch):
    """edit returns 'not_modified' -> NO new message, id unchanged (no dupe)."""
    tid = _mk_task(stage="development")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    from src.db import set_tracker_message_id, get_tracker_message_id
    set_tracker_message_id(tid, 100)

    monkeypatch.setattr(N, "edit_telegram", lambda mid, text: N.EDIT_NOT_MODIFIED)
    monkeypatch.setattr(N, "send_telegram",
                        lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not send on not_modified")))

    N.update_task_tracker(tid)
    assert get_tracker_message_id(tid) == 100  # unchanged, no duplicate


def test_transient_edit_failure_does_not_send_new_message(monkeypatch):
    """edit returns 'failed' (network/timeout/5xx) -> NO new message (no dupe)."""
    tid = _mk_task(stage="development")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    from src.db import set_tracker_message_id, get_tracker_message_id
    set_tracker_message_id(tid, 100)

    monkeypatch.setattr(N, "edit_telegram", lambda mid, text: N.EDIT_FAILED)
    monkeypatch.setattr(N, "send_telegram",
                        lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not send on transient failure")))

    N.update_task_tracker(tid)
    assert get_tracker_message_id(tid) == 100  # unchanged, no duplicate


# --------------------------------------------------------------------------- #
# edit_telegram outcome classification (httpx mocked)
# --------------------------------------------------------------------------- #
def _edit_resp(ok, description=None):
    resp = MagicMock()
    body = {"ok": ok}
    if description is not None:
        body["description"] = description
    resp.json.return_value = body
    return resp


def _patch_tg_creds(monkeypatch):
    monkeypatch.setattr(N._get_settings(), "telegram_bot_token", "T", raising=False)
    monkeypatch.setattr(N._get_settings(), "telegram_chat_id", "C", raising=False)


def test_edit_telegram_ok(monkeypatch):
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.return_value = _edit_resp(True)
        assert N.edit_telegram(1, "x") == N.EDIT_OK


def test_edit_telegram_not_modified_is_success(monkeypatch):
    # 400 "message is not modified" -> success, not gone, no duplicate
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.return_value = _edit_resp(
            False, "Bad Request: message is not modified: ...")
        assert N.edit_telegram(1, "x") == N.EDIT_NOT_MODIFIED


def test_edit_telegram_exactly_the_same_is_not_modified(monkeypatch):
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.return_value = _edit_resp(
            False, "Bad Request: specified new message content and reply markup "
                   "are exactly the same")
        assert N.edit_telegram(1, "x") == N.EDIT_NOT_MODIFIED


def test_edit_telegram_message_not_found_is_gone(monkeypatch):
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.return_value = _edit_resp(
            False, "Bad Request: message to edit not found")
        assert N.edit_telegram(1, "x") == N.EDIT_GONE


def test_edit_telegram_cant_be_edited_is_gone(monkeypatch):
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.return_value = _edit_resp(
            False, "Bad Request: message can't be edited")
        assert N.edit_telegram(1, "x") == N.EDIT_GONE


def test_edit_telegram_unknown_400_is_failed(monkeypatch):
    # unknown 400 -> failed (NOT gone) -> caller won't duplicate
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.return_value = _edit_resp(
            False, "Bad Request: some other unexpected error")
        assert N.edit_telegram(1, "x") == N.EDIT_FAILED


def test_edit_telegram_timeout_is_failed(monkeypatch):
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.side_effect = Exception("read timeout")
        assert N.edit_telegram(1, "x") == N.EDIT_FAILED


def test_edit_telegram_5xx_is_failed(monkeypatch):
    # Telegram 5xx still returns ok:false w/o gone/not_modified markers
    _patch_tg_creds(monkeypatch)
    with patch("src.notifications.httpx") as hx:
        hx.post.return_value = _edit_resp(False, "Internal Server Error")
        assert N.edit_telegram(1, "x") == N.EDIT_FAILED


# --------------------------------------------------------------------------- #
# render: repeated stage attempt shows "попытка N"
# --------------------------------------------------------------------------- #
_POPYTKA = "\u043f\u043e\u043f\u044b\u0442\u043a\u0430"  # popytka


def test_render_active_stage_shows_attempt_on_second_run():
    # Two reviewer runs while in review -> active line shows attempt 2.
    tid = _mk_task(stage="review")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    _mk_run(tid, "developer", "2026-06-04 09:10:00", "2026-06-04 09:20:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    # First review run finished (sent back to dev), second review run active.
    _mk_run(tid, "reviewer", "2026-06-04 09:20:00", "2026-06-04 09:25:00",
            in_tok=10, out_tok=5, cost=0.1, model="vibecode/claude-sonnet-4.6",
            exit_code=0)
    _mk_run(tid, "reviewer", "2026-06-04 09:30:00", None,
            in_tok=0, out_tok=0, exit_code=None)

    text = N.render_task_tracker(tid)
    active = [l for l in text.splitlines()
              if l.startswith("\U0001f504") and "Код ревью" in l][0]
    assert _POPYTKA in active
    assert "2" in active
    assert "\u0438\u0434\u0451\u0442" in active


def test_render_active_stage_no_attempt_on_first_run():
    # Single reviewer run -> active line has NO attempt marker.
    tid = _mk_task(stage="review")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    _mk_run(tid, "developer", "2026-06-04 09:10:00", "2026-06-04 09:20:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    _mk_run(tid, "reviewer", "2026-06-04 09:20:00", None,
            in_tok=0, out_tok=0, exit_code=None)

    text = N.render_task_tracker(tid)
    active = [l for l in text.splitlines()
              if l.startswith("\U0001f504") and "Код ревью" in l][0]
    assert _POPYTKA not in active
    assert "\u0438\u0434\u0451\u0442" in active


def test_render_finished_lines_unaffected_by_attempt_logic():
    # Completed (checkmark) lines never carry an attempt marker.
    tid = _mk_task(stage="review")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    # developer ran twice (retry) but is a FINISHED stage now.
    _mk_run(tid, "developer", "2026-06-04 09:10:00", "2026-06-04 09:15:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    _mk_run(tid, "developer", "2026-06-04 09:16:00", "2026-06-04 09:20:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    text = N.render_task_tracker(tid)
    for l in text.splitlines():
        if l.startswith("\u2705"):
            assert _POPYTKA not in l


# --------------------------------------------------------------------------- #
# which alerts are SEPARATE vs tracker-only
# --------------------------------------------------------------------------- #
def test_approve_gate_sends_separate_message_and_starts_brd_clock(monkeypatch):
    tid = _mk_task(stage="analysis")
    calls = []
    monkeypatch.setattr(N, "send_telegram",
                        lambda text, disable_notification=False: calls.append((text, disable_notification)) or 1)
    monkeypatch.setattr(N, "update_task_tracker", lambda task_id: None)

    N.notify_approve_requested(tid)

    # exactly one SEPARATE (notifying) send for the approve gate
    assert len(calls) == 1
    assert calls[0][1] is False  # notifying
    assert "Approved" in calls[0][0]
    # BRD clock started
    conn = get_db()
    row = conn.execute("SELECT brd_review_started_at FROM tasks WHERE id=?", (tid,)).fetchone()
    conn.close()
    assert row[0] is not None


def test_error_sends_separate_message(monkeypatch):
    tid = _mk_task(stage="development")
    calls = []
    monkeypatch.setattr(N, "send_telegram",
                        lambda text, disable_notification=False: calls.append((text, disable_notification)) or 1)
    N.notify_error(tid, "boom")
    assert len(calls) == 1
    assert calls[0][1] is False  # notifying
    assert "ERROR" in calls[0][0]


def test_stage_change_does_not_send_separate_message(monkeypatch):
    tid = _mk_task(stage="development")
    sent = []
    monkeypatch.setattr(N, "send_telegram",
                        lambda text, disable_notification=False: sent.append(text) or 1)
    # tracker refresh is allowed (edit/send silent) but must NOT use send_telegram
    # for a separate notification; stub update to isolate.
    refreshed = []
    monkeypatch.setattr(N, "update_task_tracker", lambda task_id: refreshed.append(task_id))

    N.notify_stage_change(tid, "development", "review")
    assert sent == []            # no separate message
    assert refreshed == [tid]    # tracker refreshed instead


def test_agent_started_does_not_send_separate_message(monkeypatch):
    tid = _mk_task(stage="analysis")
    sent = []
    monkeypatch.setattr(N, "send_telegram",
                        lambda text, disable_notification=False: sent.append(text) or 1)
    refreshed = []
    monkeypatch.setattr(N, "update_task_tracker", lambda task_id: refreshed.append(task_id))

    N.notify_agent_started(1, "analyst", tid)
    assert sent == []
    assert refreshed == [tid]


def test_qg_failure_does_not_send_separate_message(monkeypatch):
    tid = _mk_task(stage="development")
    sent = []
    monkeypatch.setattr(N, "send_telegram",
                        lambda text, disable_notification=False: sent.append(text) or 1)
    N.notify_qg_failure(tid, "development", "check_ci_green", "CI state: pending")
    assert sent == []  # QG-pending is log-only, never a separate ping


# --------------------------------------------------------------------------- #
# ORCH-042: mode resolution + text changes (edit-mode default)
# --------------------------------------------------------------------------- #
def _brd_line(text):
    return [ln for ln in text.splitlines() if "Подтверждение BRD" in ln][0]


def test_unknown_mode_falls_back_to_edit_branch(monkeypatch):
    """TC-02/AC-2: garbage mode -> edit branch, no exception, no extra send."""
    monkeypatch.setattr(N._get_settings(), "tracker_mode", "garbage", raising=False)
    tid = _mk_task(stage="development")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    from src.db import set_tracker_message_id, get_tracker_message_id
    set_tracker_message_id(tid, 777)

    edited = {}
    monkeypatch.setattr(N, "edit_telegram",
                        lambda mid, text: edited.update(mid=mid) or N.EDIT_OK)
    monkeypatch.setattr(N, "send_telegram",
                        lambda *a, **k: (_ for _ in ()).throw(
                            AssertionError("garbage mode must take edit branch")))
    monkeypatch.setattr(N, "delete_telegram",
                        lambda *a, **k: (_ for _ in ()).throw(
                            AssertionError("garbage mode must NOT bump-delete")))

    N.update_task_tracker(tid)  # must not raise
    assert edited["mid"] == 777
    assert get_tracker_message_id(tid) == 777  # unchanged


def test_render_brd_label_is_confirmation_not_review():
    """TC-18/AC-15: 'Подтверждение BRD' present, 'Ревью БРД' absent."""
    tid = _mk_task(stage="architecture", brd_start="2026-06-04 10:00:00",
                   brd_end="2026-06-04 10:08:00")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    text = N.render_task_tracker(tid)
    assert "Подтверждение BRD" in text
    assert "Ревью БРД" not in text


def test_render_brd_passed_uses_check_not_pause():
    """TC-19/AC-16: approve-gate passed (ended set) -> BRD line starts with ✅."""
    tid = _mk_task(stage="architecture", brd_start="2026-06-04 10:00:00",
                   brd_end="2026-06-04 10:08:00")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    line = _brd_line(N.render_task_tracker(tid))
    assert line.startswith("✅")        # ✅
    assert not line.startswith("⏸")     # not ⏸️
    assert "⏳" not in line              # no hourglass once passed


def test_render_brd_waiting_keeps_pause_and_hourglass():
    """TC-20/AC-16: still waiting (ended empty) -> ⏳ indicator, not ✅."""
    tid = _mk_task(stage="analysis", brd_start="2026-06-04 10:00:00",
                   brd_end=None)
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1)
    line = _brd_line(N.render_task_tracker(tid))
    assert "⏳" in line                  # ⏳ still waiting
    assert not line.startswith("✅")      # NOT ✅ yet


def test_render_stage_labels_are_russian():
    """TC-21/AC-17: russian stage labels in both ✅- and 🔄-lines; no english."""
    tid = _mk_task(stage="deploy")
    _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    _mk_run(tid, "architect", "2026-06-04 09:10:00", "2026-06-04 09:20:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    _mk_run(tid, "developer", "2026-06-04 09:20:00", "2026-06-04 09:30:00",
            in_tok=10, out_tok=5, cost=0.1, model="tokenator/claude-opus-4-8")
    _mk_run(tid, "reviewer", "2026-06-04 09:30:00", "2026-06-04 09:35:00",
            in_tok=10, out_tok=5, cost=0.1, model="vibecode/claude-sonnet-4.6")
    _mk_run(tid, "tester", "2026-06-04 09:35:00", "2026-06-04 09:40:00",
            in_tok=10, out_tok=5, cost=0.1, model="vibecode/claude-sonnet-4.6")
    _mk_run(tid, "deployer", "2026-06-04 09:40:00", None,
            in_tok=0, out_tok=0, exit_code=None)
    text = N.render_task_tracker(tid)
    for ru in ("Анализ", "Архитектура", "Разработка", "Код ревью",
               "Тестирование", "Внедрение"):
        assert ru in text, f"missing russian label {ru!r}"
    # ORCH-067: the new '📍 <Plane-status>' line intentionally carries the ENGLISH
    # ORCH-066 Plane status name (e.g. 'Awaiting Deploy'); the russian-only rule
    # (BR-11) applies to the STAGE label lines, so exclude the status line here.
    stage_lines = "\n".join(
        ln for ln in text.splitlines() if not ln.startswith("\U0001f4cd")
    )
    for en in ("Analysis", "Architecture", "Development", "Review",
               "Testing", "Deploy"):
        assert en not in stage_lines, f"english label leaked: {en!r}"


def test_render_done_says_vnedreno_not_deployed():
    """TC-22/AC-18: final line says '📦 Внедрено', not 'deployed'."""
    tid = _mk_task(stage="done")
    conn = get_db()
    conn.execute(
        "UPDATE tasks SET created_at='2026-06-04 09:00:00', "
        "updated_at='2026-06-04 09:56:00' WHERE id=?", (tid,))
    conn.commit()
    conn.close()
    _mk_run(tid, "deployer", "2026-06-04 09:50:00", "2026-06-04 09:56:00",
            in_tok=400, out_tok=22400, cost=1.73, model="tokenator/claude-opus-4-8")
    with patch("src.notifications.httpx") as _hx:
        _resp = MagicMock(status_code=200)
        _resp.json.return_value = []  # no PR
        _hx.get.return_value = _resp
        text = N.render_task_tracker(tid)
    assert "\U0001f4e6 Внедрено" in text  # 📦 Внедрено
    assert "deployed" not in text
