"""Один бот — несколько проектов (R18).

Владелец сказал дословно: «Один бот — несколько проектов».
Опрос Телеграма живёт в сервере панели одного проекта, и два проекта на один
токен не должны растаскивать общую очередь getUpdates и терять команды.
Один процесс владеет опросом, а бот видит и обслуживает прогоны всех
зарегистрированных проектов.

Проверяются ключевые правила:
1. Реестр проектов: register_project / unregister_project / registered_projects
   сохраняют каталоги без дублей и отсекают несуществующие.
2. Единство опроса (Singleton Poller Leader): claim_poller отдаёт владение
   первому процессу; второй процесс не перехватывает getUpdates, пока первый жив.
3. Перехват лидерства: если владелец опроса умирает, второй процесс забирает опрос.
4. Агрегация прогонов: all_runs, runs_text, waiting_count видят прогоны всех проектов.
5. Доставка и ответы: вопросы из любого проекта доставляются владельцу, и ответ
   через Телеграм попадает в нужный прогон нужного проекта.
"""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

import pytest

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

from chekan import ask, telegram  # noqa: E402
from chekan.store import Run, now  # noqa: E402

TOKEN = "1234567890:AAHfake-token-for-tests-0123456789x"
OWNER = 424242


class FakeTelegram:
    def __init__(self):
        self.calls: list[tuple[str, dict]] = []
        self.updates: list[dict] = []
        self._mid = 100
        self._next = 1

    def __call__(self, token: str, method: str, params: dict | None = None, timeout: int = 10):
        params = dict(params or {})
        self.calls.append((method, params))
        if method == "getMe":
            return {"username": "ceh_test_bot"}
        if method == "getUpdates":
            offset = int(params.get("offset") or 0)
            return [u for u in self.updates if u["update_id"] >= offset]
        if method == "sendMessage":
            self._mid += 1
            return {"message_id": self._mid, "chat": {"id": params.get("chat_id")}}
        if method == "editMessageText":
            return {"message_id": params.get("message_id")}
        return {}

    def say(self, text: str, chat: int = OWNER) -> dict:
        upd = {"update_id": self._next, "message": {"chat": {"id": chat}, "text": text}}
        self._next += 1
        self.updates.append(upd)
        return upd

    def sent(self) -> list[dict]:
        return [p for m, p in self.calls if m == "sendMessage"]

    def texts(self) -> list[str]:
        return [p.get("text", "") for p in self.sent()]

    def clear(self) -> None:
        self.calls.clear()


@pytest.fixture
def tg(monkeypatch) -> FakeTelegram:
    fake = FakeTelegram()
    monkeypatch.setattr(telegram, "call", fake)
    return fake


@pytest.fixture(autouse=True)
def cfg(tmp_path: Path, monkeypatch):
    monkeypatch.setenv(telegram.CONFIG_ENV, str(tmp_path / "cfg" / "telegram.json"))
    telegram.save({"token": TOKEN, "bot": "ceh_test_bot", "owner_id": str(OWNER)})
    yield
    telegram.release_poller()


def _make_repo(root: Path, name: str) -> Path:
    repo = root / name
    repo.mkdir(parents=True, exist_ok=True)
    subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
    subprocess.run(["git", "config", "user.email", "t@t"], cwd=repo, check=True)
    subprocess.run(["git", "config", "user.name", "t"], cwd=repo, check=True)
    (repo / "app.py").write_text("print('ok')\n", encoding="utf-8")
    subprocess.run(["git", "add", "-A"], cwd=repo, check=True)
    subprocess.run(["git", "commit", "-qm", "init"], cwd=repo, check=True)
    return repo


def _roots(repo: Path) -> Path:
    return repo / ".chekan"


# --- 1. Реестр проектов ---


def test_project_registry_adds_and_filters_directories(tmp_path: Path):
    repo1 = _make_repo(tmp_path, "proj1")
    repo2 = _make_repo(tmp_path, "proj2")

    telegram.register_project(_roots(repo1))
    telegram.register_project(_roots(repo2))
    telegram.register_project(_roots(repo1))  # дубль

    registered = telegram.registered_projects()
    assert len(registered) == 2
    assert _roots(repo1) in registered
    assert _roots(repo2) in registered

    telegram.unregister_project(_roots(repo1))
    assert _roots(repo1) not in telegram.registered_projects()
    assert _roots(repo2) in telegram.registered_projects()


def test_registered_projects_drops_deleted_folders(tmp_path: Path):
    repo = _make_repo(tmp_path, "temp_proj")
    _roots(repo).mkdir(parents=True, exist_ok=True)
    telegram.register_project(_roots(repo))
    assert _roots(repo) in telegram.registered_projects()

    import shutil
    shutil.rmtree(repo)
    assert _roots(repo) not in telegram.registered_projects()


# --- 2. Единство опроса (Leader Election) ---


def test_claim_poller_singleton_and_takeover(tmp_path: Path):
    """Рычаг порчи: два процесса не должны одновременно владеть опросом."""
    assert telegram.claim_poller() is True
    assert telegram.claim_poller() is True  # тот же PID может повторить

    alive, pid = telegram.is_poller_alive()
    assert alive is True
    assert pid == os.getpid()

    # Имитируем другой живой процесс (например, PID 1 - init/launchd)
    telegram.poller_pid_path().write_text("1\n", encoding="utf-8")
    assert telegram.claim_poller() is False, "второй процесс перехватил опрос у живого лидера"

    # Имитируем мёртвый PID
    telegram.poller_pid_path().write_text("9999999\n", encoding="utf-8")
    assert telegram.claim_poller() is True, "не удалось перехватить опрос у мёртвого процесса"
    assert telegram.is_poller_alive()[1] == os.getpid()


# --- 3. Агрегация прогонов по всем проектам ---


def test_all_runs_and_runs_text_across_projects(tmp_path: Path):
    repo1 = _make_repo(tmp_path, "alpha")
    repo2 = _make_repo(tmp_path, "beta")

    run1 = Run.create(repo1, "Прогон Альфа", "normal", "T1")
    run2 = Run.create(repo2, "Прогон Бета", "normal", "T1")

    telegram.register_project(_roots(repo1))
    telegram.register_project(_roots(repo2))

    all_found = telegram.all_runs()
    slugs = [r.slug for r in all_found]
    assert run1.slug in slugs
    assert run2.slug in slugs

    text = telegram.runs_text()
    assert "Прогон Альфа" in text
    assert "Прогон Бета" in text


def test_counter_and_queue_across_projects(tmp_path: Path):
    """Счётчик и очередь учитывают вопросы из всех проектов."""
    repo1 = _make_repo(tmp_path, "alpha")
    repo2 = _make_repo(tmp_path, "beta")

    run1 = Run.create(repo1, "Альфа с вопросом", "normal", "T1")
    run2 = Run.create(repo2, "Бета с вопросом", "normal", "T1")

    telegram.register_project(_roots(repo1))
    telegram.register_project(_roots(repo2))

    ask.create(run1, "Вопрос из Альфы")
    ask.create(run2, "Вопрос из Беты")

    assert telegram.waiting_count() == 2

    first_next = telegram.next_text()
    assert "Вопрос из" in first_next
    opened_first = telegram.open_slug()
    assert opened_first in (run1.slug, run2.slug)

    second_next = telegram.next_text()
    assert "Вопрос из" in second_next
    opened_second = telegram.open_slug()
    assert opened_second in (run1.slug, run2.slug)
    assert opened_first != opened_second


# --- 4. Доставка вопросов и ответы через Телеграм ---


def test_questions_delivered_and_answered_for_second_project(tg: FakeTelegram, tmp_path: Path):
    repo1 = _make_repo(tmp_path, "proj1")
    repo2 = _make_repo(tmp_path, "proj2")

    run1 = Run.create(repo1, "Первый проект", "normal", "T1")
    run2 = Run.create(repo2, "Второй проект", "normal", "T1")

    telegram.register_project(_roots(repo1))
    telegram.register_project(_roots(repo2))

    # Вопрос возникает во втором проекте
    q = ask.create(run2, "Берём ли Redis во второй проект?")
    assert run2.is_paused()

    # Доставка запускается
    sent_count = telegram.deliver_questions()
    assert sent_count == 1
    assert "Второй проект" in tg.texts()[0]
    assert "Берём ли Redis" in tg.texts()[0]

    # Владелец нажимает «Следующий»
    tg.say("Следующий")
    telegram.poll_once()
    assert telegram.open_slug() == run2.slug

    # Владелец пишет ответ
    tg.say("Да, Redis берём")
    telegram.poll_once()

    # Проверяем, что ответ попал именно во второй проект
    fresh_run2 = Run(repo2, run2.slug)
    assert not fresh_run2.is_paused()
    ans = ask.question(fresh_run2, q["id"])
    assert ans["status"] == "answered"
    assert ans["answer"]["text"] == "Да, Redis берём"
    assert ans["answer"]["by"] == "telegram"
