"""Per-send Telegram options (`disable_notification`, `message_thread_id`).

Verifies the ContextVar-based plumbing inside ``TelegramClient`` so the
two new flags actually land in the request payloads at all four send
paths (sendMessage, single-asset send, media-group, cache-hit POST) and
that concurrent ``asyncio.gather`` fan-outs in the dispatcher don't leak
options between tasks.
"""

from __future__ import annotations

import asyncio
import json
from typing import Any

import pytest
from aiohttp import FormData


def test_telegram_receiver_factory_reads_new_fields() -> None:
    """The receiver factory turns config-dict keys into typed fields."""
    from notify_bridge_core.notifications.receiver import (
        TelegramReceiver, build_receiver,
    )

    recv = build_receiver(
        "telegram",
        {
            "chat_id": "12345",
            "disable_notification": True,
            "message_thread_id": "7",  # string form, common from JSON UI
        },
    )
    assert isinstance(recv, TelegramReceiver)
    assert recv.chat_id == "12345"
    assert recv.disable_notification is True
    assert recv.message_thread_id == 7


def test_telegram_receiver_factory_defaults_when_missing() -> None:
    """Missing keys default to off / general topic."""
    from notify_bridge_core.notifications.receiver import (
        TelegramReceiver, build_receiver,
    )

    recv = build_receiver("telegram", {"chat_id": "12345"})
    assert isinstance(recv, TelegramReceiver)
    assert recv.disable_notification is False
    assert recv.message_thread_id is None


@pytest.mark.parametrize(
    "raw_thread, expected",
    [
        (None, None),
        ("", None),
        ("not-a-number", None),
        ("42", 42),
        (42, 42),
        # ``0`` is Telegram's "general topic" sentinel — collapse to None
        # so the Bot API just omits the field, matching the frontend's
        # ``<= 0 → unset`` behaviour.
        ("0", None),
        (0, None),
        (-5, None),
        # bool would otherwise pass through as int(True)==1 / int(False)==0
        # and silently route into topic #1; reject explicitly.
        (True, None),
        (False, None),
    ],
)
def test_telegram_receiver_thread_id_coercion(raw_thread: Any, expected: Any) -> None:
    from notify_bridge_core.notifications.receiver import build_receiver

    recv = build_receiver(
        "telegram",
        {"chat_id": "1", "message_thread_id": raw_thread},
    )
    assert recv.message_thread_id == expected  # type: ignore[attr-defined]


def test_apply_send_opts_to_payload_merges_when_bound() -> None:
    """Inside ``_bind_send_options``, payload helper writes the two keys."""
    from notify_bridge_core.notifications.telegram.client import (
        _SendOptions,
        _apply_send_opts_to_payload,
        _bind_send_options,
    )

    payload: dict[str, Any] = {"chat_id": "1"}
    with _bind_send_options(_SendOptions(disable_notification=True, message_thread_id=7)):
        _apply_send_opts_to_payload(payload)
    assert payload["disable_notification"] is True
    assert payload["message_thread_id"] == 7


def test_apply_send_opts_to_payload_omits_when_default() -> None:
    """No bind = no flags written (Bot API treats omission as default)."""
    from notify_bridge_core.notifications.telegram.client import (
        _apply_send_opts_to_payload,
    )

    payload: dict[str, Any] = {"chat_id": "1"}
    _apply_send_opts_to_payload(payload)
    assert "disable_notification" not in payload
    assert "message_thread_id" not in payload


def test_apply_send_opts_to_form_merges_when_bound() -> None:
    """Multipart payload helper writes the two fields when bound."""
    from notify_bridge_core.notifications.telegram.client import (
        _SendOptions,
        _apply_send_opts_to_form,
        _bind_send_options,
    )

    form = FormData()
    with _bind_send_options(_SendOptions(disable_notification=True, message_thread_id=42)):
        _apply_send_opts_to_form(form)

    # aiohttp.FormData stores fields as ``(MultiDict{name, ...}, headers, value)``.
    name_to_value = {}
    for type_opts, _headers, value in form._fields:  # type: ignore[attr-defined]
        name_to_value[type_opts.get("name")] = value
    assert name_to_value.get("disable_notification") == "true"
    assert name_to_value.get("message_thread_id") == "42"


def test_bind_send_options_resets_on_exit() -> None:
    """Token-reset semantics: the var is restored even after a raise."""
    from notify_bridge_core.notifications.telegram.client import (
        _SendOptions,
        _bind_send_options,
        _send_options_var,
    )

    default = _send_options_var.get()
    try:
        with _bind_send_options(_SendOptions(disable_notification=True)):
            raise RuntimeError("boom")
    except RuntimeError:
        pass
    assert _send_options_var.get() == default


@pytest.mark.asyncio
async def test_concurrent_binds_do_not_leak_between_tasks() -> None:
    """Two ``asyncio.gather`` tasks see only their own bound options.

    This is the load-bearing invariant for the dispatcher's per-receiver
    fan-out: one chat with ``disable_notification=True`` must not silence
    a peer chat in the same dispatch.
    """
    from notify_bridge_core.notifications.telegram.client import (
        _SendOptions,
        _apply_send_opts_to_payload,
        _bind_send_options,
    )

    results: list[dict[str, Any]] = []

    async def run_with(opts: _SendOptions, label: str) -> None:
        payload: dict[str, Any] = {"label": label}
        with _bind_send_options(opts):
            # Yield to the loop to interleave with the sibling task.
            await asyncio.sleep(0)
            _apply_send_opts_to_payload(payload)
        results.append(payload)

    await asyncio.gather(
        run_with(_SendOptions(disable_notification=True, message_thread_id=1), "silent"),
        run_with(_SendOptions(disable_notification=False, message_thread_id=2), "loud"),
    )

    by_label = {r["label"]: r for r in results}
    assert by_label["silent"].get("disable_notification") is True
    assert by_label["silent"].get("message_thread_id") == 1
    assert "disable_notification" not in by_label["loud"]  # False → omitted
    assert by_label["loud"].get("message_thread_id") == 2


@pytest.mark.asyncio
async def test_send_message_passes_options_into_payload(monkeypatch) -> None:
    """``send_message(disable_notification=True, message_thread_id=N)``
    surfaces both keys in the JSON request body."""
    from notify_bridge_core.notifications.telegram.client import TelegramClient

    captured: dict[str, Any] = {}

    class _FakeResp:
        status = 200

        async def json(self) -> dict[str, Any]:
            return {"ok": True, "result": {"message_id": 99}}

        async def __aenter__(self) -> "_FakeResp":
            return self

        async def __aexit__(self, *args: Any) -> None:
            return None

    class _FakeSession:
        def post(self, url: str, *, json: dict[str, Any] | None = None, **_kw: Any) -> _FakeResp:
            captured["url"] = url
            captured["json"] = json
            return _FakeResp()

    client = TelegramClient(_FakeSession(), "TEST:token")  # type: ignore[arg-type]
    result = await client.send_message(
        chat_id="123",
        text="hello",
        disable_notification=True,
        message_thread_id=5,
    )
    assert result["success"] is True
    payload = captured["json"]
    assert payload["disable_notification"] is True
    assert payload["message_thread_id"] == 5


@pytest.mark.asyncio
async def test_send_message_without_options_omits_keys(monkeypatch) -> None:
    """Default kwargs leave the payload Bot-API-clean."""
    from notify_bridge_core.notifications.telegram.client import TelegramClient

    captured: dict[str, Any] = {}

    class _FakeResp:
        status = 200

        async def json(self) -> dict[str, Any]:
            return {"ok": True, "result": {"message_id": 1}}

        async def __aenter__(self) -> "_FakeResp":
            return self

        async def __aexit__(self, *args: Any) -> None:
            return None

    class _FakeSession:
        def post(self, url: str, *, json: dict[str, Any] | None = None, **_kw: Any) -> _FakeResp:
            captured["json"] = json
            return _FakeResp()

    client = TelegramClient(_FakeSession(), "TEST:token")  # type: ignore[arg-type]
    await client.send_message(chat_id="123", text="hello")
    payload = captured["json"]
    assert "disable_notification" not in payload
    assert "message_thread_id" not in payload
