import json
import sqlite3
import sys
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from mars_bot.bot import run_check


@pytest.fixture(autouse=True)
def _no_sleep(monkeypatch):
    """All bot tests run with REQUEST_DELAY=0 to keep the suite fast."""
    monkeypatch.setattr("mars_bot.bot.REQUEST_DELAY", 0)


def _setup_project(tmp_path: Path) -> Path:
    (tmp_path / ".env").write_text(
        "TELEGRAM_BOT_TOKEN=tg-token\nTGSTAT_TOKEN=tgstat-token\n"
    )
    (tmp_path / "chats.json").write_text(json.dumps({
        "test": 42,
        "team": -100,
        "_mentions": {"tracked": {"marsingru": "team", "natashhhh": "test"}},
    }))
    return tmp_path


SAMPLE_TGSTAT_RESPONSE = {
    "items": [
        {"mentionId": 1, "mentionType": "channel", "postId": 1001,
         "postLink": "https://t.me/a/1", "postDate": 1700000000, "channelId": 100},
        {"mentionId": 2, "mentionType": "channel", "postId": 1002,
         "postLink": "https://t.me/b/2", "postDate": 1700000001, "channelId": 200},
    ],
    "channels": [
        {"id": 100, "username": "@a", "title": "A", "participants_count": 1000,
         "link": "t.me/a"},
        {"id": 200, "username": "@b", "title": "B", "participants_count": 2000,
         "link": "t.me/b"},
    ],
}


def test_cold_start_fills_db_no_send(monkeypatch, tmp_path: Path):
    """При cold start (нет mentions.db) первый прогон молча заполняет, ничего не шлёт."""
    root = _setup_project(tmp_path)
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    send_mock = MagicMock()
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    exit_code = run_check(root, dry_run=False)
    assert exit_code == 0
    assert send_mock.call_count == 0
    db_path = root / "data" / "mentions.db"
    assert db_path.exists()
    with sqlite3.connect(db_path) as conn:
        rows = conn.execute("SELECT post_id, alerted FROM mentions").fetchall()
        # Составной ключ (post_id, mars_channel): 2 tracked-канала × 2 поста = 4 строки
        # (один и тот же пост записывается отдельно под каждый канал Марса).
        assert len(rows) == 4
        assert all(alerted == 1 for _, alerted in rows)
        # Маркер завершённого cold start выставлен.
        marker = conn.execute(
            "SELECT value FROM meta WHERE key='cold_start_done'"
        ).fetchone()
        assert marker == ("1",)


def _pre_init_db(root: Path) -> None:
    """Создаёт mentions.db с выставленным cold-start маркером — чтобы run_check
    работал в НОРМАЛЬНОМ режиме (без маркера пустая БД считается cold start)."""
    from mars_bot.mentions_db import init, mark_cold_start_done
    db_path = root / "data" / "mentions.db"
    init(db_path)
    with sqlite3.connect(db_path) as conn:
        mark_cold_start_done(conn)


def test_normal_pass_inserts_and_sends(monkeypatch, tmp_path: Path):
    root = _setup_project(tmp_path)
    _pre_init_db(root)
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    run_check(root, dry_run=False)
    # 2 поста × 2 tracked-канала (составной ключ) → 4 отправки.
    assert send_mock.call_count == 4


def test_dedup_second_pass_sends_nothing(monkeypatch, tmp_path: Path):
    root = _setup_project(tmp_path)
    _pre_init_db(root)
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    run_check(root, dry_run=False)
    send_mock.reset_mock()
    run_check(root, dry_run=False)
    assert send_mock.call_count == 0


def test_self_mention_skipped(monkeypatch, tmp_path: Path):
    root = _setup_project(tmp_path)
    _pre_init_db(root)
    self_resp = {
        "items": [{"mentionId": 99, "mentionType": "channel", "postId": 9999,
                   "postLink": "https://t.me/marsingru/100",
                   "postDate": 1700000000, "channelId": 555}],
        "channels": [{"id": 555, "username": "@MARSINGRU", "title": "Mars",
                      "participants_count": 100, "link": "t.me/marsingru"}],
    }
    monkeypatch.setattr("mars_bot.bot.tgstat.get_mentions", lambda **kw: self_resp)
    send_mock = MagicMock()
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    run_check(root, dry_run=False)
    assert send_mock.call_count == 0
    with sqlite3.connect(root / "data" / "mentions.db") as conn:
        rows = conn.execute("SELECT COUNT(*) FROM mentions").fetchall()
        assert rows == [(0,)]


def test_send_failure_keeps_alerted_zero(monkeypatch, tmp_path: Path):
    root = _setup_project(tmp_path)
    _pre_init_db(root)
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    from mars_bot.telegram import TelegramError
    monkeypatch.setattr(
        "mars_bot.bot.telegram.send_message",
        MagicMock(side_effect=TelegramError("boom")),
    )

    run_check(root, dry_run=False)
    with sqlite3.connect(root / "data" / "mentions.db") as conn:
        rows = conn.execute("SELECT alerted FROM mentions").fetchall()
        assert all(a == 0 for (a,) in rows)


def test_retry_pass_resends_failed(monkeypatch, tmp_path: Path):
    root = _setup_project(tmp_path)
    _pre_init_db(root)
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    from mars_bot.telegram import TelegramError
    monkeypatch.setattr(
        "mars_bot.bot.telegram.send_message",
        MagicMock(side_effect=TelegramError("boom")),
    )
    run_check(root, dry_run=False)

    send_ok = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_ok)
    run_check(root, dry_run=False)

    # 2 поста × 2 канала = 4 недосланных строки → 4 повторные отправки в pre-pass.
    assert send_ok.call_count == 4
    with sqlite3.connect(root / "data" / "mentions.db") as conn:
        rows = conn.execute("SELECT alerted FROM mentions").fetchall()
        assert all(a == 1 for (a,) in rows)


def test_dry_run_has_no_side_effects(monkeypatch, tmp_path: Path, capsys):
    root = _setup_project(tmp_path)
    # БД НЕ преинициализируем — cold start не должен сработать в dry-run.
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    send_mock = MagicMock()
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    run_check(root, dry_run=True)
    out = capsys.readouterr().out
    assert "#mention" in out
    assert send_mock.call_count == 0
    assert not (root / "data" / "mentions.db").exists()


def test_multi_channel_both_alert(monkeypatch, tmp_path: Path):
    """Пост, упомянувший marsingru И natashhhh, шлёт ДВА алерта — по одному в чат
    каждого канала Марса (регрессия: раньше составной пост давал один алерт)."""
    root = _setup_project(tmp_path)  # marsingru → team, natashhhh → test
    _pre_init_db(root)

    shared = {
        "items": [{"mentionId": 1, "mentionType": "channel", "postId": 7777,
                   "postLink": "https://t.me/blogger/1", "postDate": 1700000000,
                   "channelId": 50}],
        "channels": [{"id": 50, "username": "@blogger", "title": "Blogger",
                      "participants_count": 500, "link": "t.me/blogger"}],
    }
    monkeypatch.setattr("mars_bot.bot.tgstat.get_mentions", lambda **kw: shared)
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    run_check(root, dry_run=False)
    assert send_mock.call_count == 2

    from mars_bot.config import load_config
    cfg = load_config(root)
    dest_chats = {call.args[1] for call in send_mock.call_args_list}
    assert dest_chats == {cfg.chats["team"], cfg.chats["test"]}

    with sqlite3.connect(root / "data" / "mentions.db") as conn:
        channels = {
            r[0] for r in conn.execute(
                "SELECT mars_channel FROM mentions WHERE post_id=7777"
            ).fetchall()
        }
        assert channels == {"marsingru", "natashhhh"}


def test_malformed_item_skipped_not_crash(monkeypatch, tmp_path: Path):
    """Item без postId не должен ронять весь цикл — пропускаем + лог в stderr."""
    root = _setup_project(tmp_path)
    _pre_init_db(root)
    malformed = {
        "items": [
            # один битый item (нет postId) + один нормальный
            {"mentionId": 1, "mentionType": "channel",
             "postLink": "https://t.me/x/1", "postDate": 1700000000, "channelId": 9},
            {"mentionId": 2, "mentionType": "channel", "postId": 200,
             "postLink": "https://t.me/x/2", "postDate": 1700000001, "channelId": 9},
        ],
        "channels": [{"id": 9, "username": "@x", "title": "X",
                      "participants_count": 100, "link": "t.me/x"}],
    }
    monkeypatch.setattr("mars_bot.bot.tgstat.get_mentions", lambda **kw: malformed)
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    exit_code = run_check(root, dry_run=False)
    assert exit_code == 0
    # Битый item пропущен; нормальный (postId=200) обработан. Составной ключ:
    # 2 tracked-канала × 1 нормальный item = 2 строки → 2 отправки.
    assert send_mock.call_count == 2


def test_dry_run_prints_summary(monkeypatch, tmp_path: Path, capsys):
    """Dry-run должен в конце печатать сводку 'would-send=N'."""
    root = _setup_project(tmp_path)
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", MagicMock())

    run_check(root, dry_run=True)
    out = capsys.readouterr().out
    # 2 unique posts, mentioned for both tracked channels → 2 unique cards rendered
    # × 2 tracked = 4 yields, но dry-run не дедупит, печатает каждый. Считать ровно
    # количество не требуем — просто проверяем что summary line есть и >0.
    assert "[dry-run] would-send=" in out


# --- Issue 1: watcher не должен молча умирать ---

def test_all_tgstat_fail_returns_nonzero_and_alerts_owner(monkeypatch, tmp_path: Path):
    """Если TGStat упал ПО ВСЕМ каналам — ненулевой exit + алерт в тестовый чат."""
    root = _setup_project(tmp_path)
    _pre_init_db(root)

    from mars_bot.tgstat import TGStatError

    def boom(**kw):
        raise TGStatError("HTTP request failed")
    monkeypatch.setattr("mars_bot.bot.tgstat.get_mentions", boom)
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    exit_code = run_check(root, dry_run=False)
    assert exit_code == 1  # systemd увидит failed, а не success

    # Ровно один алерт — в тестовый чат (fallback alias 'test'), НЕ в рабочие.
    from mars_bot.config import load_config
    cfg = load_config(root)
    assert send_mock.call_count == 1
    assert send_mock.call_args.args[1] == cfg.chats["test"]
    assert "TGStat" in send_mock.call_args.args[2]


def test_partial_tgstat_fail_still_succeeds(monkeypatch, tmp_path: Path):
    """Если упал только ОДИН из каналов — прогон успешен (exit 0), алерта о сбое нет."""
    root = _setup_project(tmp_path)  # tracked: marsingru, natashhhh
    _pre_init_db(root)

    from mars_bot.tgstat import TGStatError

    def flaky(**kw):
        if kw["channel_id"] == "@marsingru":
            raise TGStatError("HTTP request failed")
        return SAMPLE_TGSTAT_RESPONSE
    monkeypatch.setattr("mars_bot.bot.tgstat.get_mentions", flaky)
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    exit_code = run_check(root, dry_run=False)
    assert exit_code == 0
    # natashhhh отработал (2 поста), алерта о сбое (⚠️) среди отправок нет.
    assert send_mock.call_count == 2
    assert all("⚠️" not in c.args[2] for c in send_mock.call_args_list)


def test_owner_alert_never_targets_work_chats(monkeypatch, tmp_path: Path):
    """Алерт о сбое идёт в 'test', даже когда рабочие чаты (team) есть в конфиге."""
    root = _setup_project(tmp_path)
    _pre_init_db(root)
    from mars_bot.tgstat import TGStatError
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        MagicMock(side_effect=TGStatError("down")),
    )
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    run_check(root, dry_run=False)
    from mars_bot.config import load_config
    cfg = load_config(root)
    targets = {c.args[1] for c in send_mock.call_args_list}
    assert cfg.chats["team"] not in targets


def test_crashed_cold_start_does_not_spam_history(monkeypatch, tmp_path: Path):
    """Cold start, упавший до маркера, при рестарте снова холодный — историю не шлёт."""
    root = _setup_project(tmp_path)
    monkeypatch.setattr(
        "mars_bot.bot.tgstat.get_mentions",
        lambda **kw: SAMPLE_TGSTAT_RESPONSE,
    )
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    # Первый (cold) прогон заполняет БД и ставит маркер.
    run_check(root, dry_run=False)
    assert send_mock.call_count == 0

    # Симулируем «падение до маркера»: удаляем маркер, оставляя данные.
    db_path = root / "data" / "mentions.db"
    with sqlite3.connect(db_path) as conn:
        conn.execute("DELETE FROM meta WHERE key='cold_start_done'")
        conn.commit()

    send_mock.reset_mock()
    run_check(root, dry_run=False)
    # Прогон снова холодный: ничего не рассылает (иначе был бы спам историей).
    assert send_mock.call_count == 0
    # Маркер восстановлен.
    with sqlite3.connect(db_path) as conn:
        assert conn.execute(
            "SELECT value FROM meta WHERE key='cold_start_done'"
        ).fetchone() == ("1",)


def test_config_error_returns_nonzero(monkeypatch):
    """ConfigError из run_check → main() завершается кодом 2, не трейсбеком."""
    from mars_bot.bot import main
    from mars_bot.config import ConfigError

    def boom(*a, **kw):
        raise ConfigError("chats.json is not valid JSON")
    monkeypatch.setattr("mars_bot.bot.run_check", boom)
    monkeypatch.setattr(sys, "argv", ["mars-bot-mentions", "check"])
    assert main() == 2


def test_missing_tgstat_token_alerts_and_raises(monkeypatch, tmp_path: Path):
    """Нет TGSTAT_TOKEN → алерт владелице + MentionsRunError (exit 2 в main)."""
    from mars_bot.bot import MentionsRunError
    # load_dotenv(override=True) не трогает ключи, которых нет в .env — сбрасываем
    # возможную утечку TGSTAT_TOKEN из os.environ от предыдущих тестов.
    monkeypatch.delenv("TGSTAT_TOKEN", raising=False)
    (tmp_path / ".env").write_text("TELEGRAM_BOT_TOKEN=t\n")  # нет TGSTAT_TOKEN
    (tmp_path / "chats.json").write_text(json.dumps({
        "test": 42, "team": -100,
        "_mentions": {"tracked": {"marsingru": "team", "natashhhh": "test"}},
    }))
    send_mock = MagicMock(return_value={"message_id": 1})
    monkeypatch.setattr("mars_bot.bot.telegram.send_message", send_mock)

    with pytest.raises(MentionsRunError):
        run_check(tmp_path, dry_run=False)
    # Алерт ушёл в тестовый чат.
    assert send_mock.call_count == 1
    assert send_mock.call_args.args[1] == 42
