"""Unit tests for PushService: payload, cooldown, dead-subscription pruning."""

import asyncio
from types import SimpleNamespace


from navi.push.service import PushService, _preview


class FakeStore:
    def __init__(self, subs=None):
        self.subs = subs or []
        self.deleted: list[str] = []
        self.pushed: list[str] = []

    async def upsert(self, user_id, endpoint, p256dh, auth, user_agent=None):
        self.upserts = getattr(self, "upserts", [])
        self.upserts.append((user_id, endpoint, p256dh, auth, user_agent))
        return SimpleNamespace(id="new-id", user_id=user_id, endpoint=endpoint,
                               p256dh=p256dh, auth=auth, user_agent=user_agent)

    async def list_for_user(self, user_id):
        return list(self.subs)

    async def delete(self, endpoint):
        self.deleted.append(endpoint)

    async def mark_pushed(self, sub_id):
        self.pushed.append(sub_id)


def make_settings(**over):
    defaults = dict(
        navi_push_vapid_public_key="PUB",
        navi_push_vapid_private_key="PRIV",
        navi_push_vapid_subject="mailto:test@navi.local",
        navi_push_cooldown_sec=30,
    )
    defaults.update(over)
    return SimpleNamespace(**defaults)


def make_sub(endpoint="https://push.example/ep1", sub_id="s1"):
    return SimpleNamespace(
        id=sub_id, user_id="u1", endpoint=endpoint, p256dh="P", auth="A",
        user_agent=None,
    )


async def drain():
    """Let scheduled create_task fan-outs finish (to_thread needs real time)."""
    for _ in range(20):
        await asyncio.sleep(0.01)


def test_preview_strips_markdown_and_truncates():
    text = _preview("# Heading\n\n**bold** and `code` [link](http://x)\n\n```py\nblock = 1\n```\nplain")
    assert "Heading" in text
    assert "**" not in text
    assert "`" not in text
    assert "block" not in text  # fenced code dropped

    long = _preview("x" * 500)
    assert len(long) <= 140
    assert long.endswith("…")


async def test_disabled_service_is_noop(monkeypatch):
    store = FakeStore([make_sub()])
    service = PushService(store, make_settings(navi_push_vapid_public_key=""))
    await service.notify_turn_complete("sess1", "u1", "hello")
    await drain()
    assert store.pushed == []


async def test_no_user_id_is_noop():
    store = FakeStore([make_sub()])
    service = PushService(store, make_settings())
    await service.notify_turn_complete("sess1", None, "hello")
    await drain()
    assert store.pushed == []


async def test_send_ok_marks_pushed(monkeypatch):
    store = FakeStore([make_sub()])
    service = PushService(store, make_settings())
    calls = []

    def fake_webpush(subscription_info, data, vapid_private_key, vapid_claims):
        calls.append((subscription_info, data, vapid_private_key, vapid_claims))

    import pywebpush
    monkeypatch.setattr(pywebpush, "webpush", fake_webpush)

    await service.notify_turn_complete("sess1", "u1", "Ответ готов")
    await drain()

    assert len(calls) == 1
    assert calls[0][2] == "PRIV"
    assert calls[0][3] == {"sub": "mailto:test@navi.local"}
    assert store.pushed == ["s1"]


async def test_404_prunes_dead_subscription(monkeypatch):
    store = FakeStore([make_sub()])
    service = PushService(store, make_settings())

    class FakeResponse:
        status_code = 404

    class WebPushException(Exception):
        def __init__(self):
            self.response = FakeResponse()

    import pywebpush

    def failing_webpush(*a, **kw):
        raise WebPushException()

    monkeypatch.setattr(pywebpush, "webpush", failing_webpush)

    await service.notify_turn_complete("sess1", "u1", "hello")
    await drain()

    assert store.deleted == ["https://push.example/ep1"]
    assert store.pushed == []


async def test_other_errors_keep_subscription(monkeypatch):
    store = FakeStore([make_sub()])
    service = PushService(store, make_settings())

    import pywebpush

    def failing_webpush(*a, **kw):
        raise ConnectionError("boom")

    monkeypatch.setattr(pywebpush, "webpush", failing_webpush)

    await service.notify_turn_complete("sess1", "u1", "hello")
    await drain()

    assert store.deleted == []
    assert store.pushed == []


async def test_cooldown_suppresses_rapid_fire(monkeypatch):
    store = FakeStore([make_sub()])
    service = PushService(store, make_settings())
    import pywebpush

    monkeypatch.setattr(pywebpush, "webpush", lambda *a, **kw: None)

    await service.notify_turn_complete("sess1", "u1", "one")
    await drain()
    await service.notify_turn_complete("sess1", "u1", "two")
    await drain()
    # Different session is not suppressed.
    await service.notify_turn_complete("sess2", "u1", "three")
    await drain()

    assert len(store.pushed) == 2


async def test_empty_content_is_noop():
    store = FakeStore([make_sub()])
    service = PushService(store, make_settings())
    await service.notify_turn_complete("sess1", "u1", "   ")
    await drain()
    assert store.pushed == []


async def test_subscribe_and_unsubscribe_roundtrip():
    store = FakeStore()
    service = PushService(store, make_settings())
    sub_id = await service.subscribe("u1", "ep", "p", "a", "UA")
    assert sub_id == "new-id"
    assert store.upserts == [("u1", "ep", "p", "a", "UA")]
    await service.unsubscribe("ep")
    assert store.deleted == ["ep"]