import asyncio
import json
from datetime import datetime, timezone

import pytest

from src import gitea_proxy
from src.push_notifications import PushConfiguration, dispatch_deadline_reminders
from src.push_subscription_store import PushSubscriptionStore


@pytest.mark.anyio
async def test_deadline_reminder_sends_one_private_local_day_digest_and_deduplicates(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    store.set_deadline_preferences(
        "device-a", enabled=True, timezone="America/New_York", reminder_hour=9
    )
    sent = []

    async def assigned():
        return {
            "complete": True,
            "items": [
                {
                    "id": 42,
                    "title": "Private launch plan",
                    "repository": {"full_name": "private/repo"},
                    "due_date": "2026-08-14T12:00:00Z",
                },
                {"id": 43, "due_date": "2026-08-20T12:00:00Z"},
            ],
        }

    async def send(_subscription, payload):
        sent.append(json.loads(payload))

    config = PushConfiguration("public", "private", "mailto:ops@example.com")
    now = datetime(2026, 8, 13, 13, 5, tzinfo=timezone.utc)

    assert await dispatch_deadline_reminders(store, config, assigned, send, now=now) == 1
    assert await dispatch_deadline_reminders(store, config, assigned, send, now=now) == 0
    assert sent == [{
        "title": "1 deadline needs attention",
        "body": "Open Agenda to review or replan it.",
        "route": "#/my-work/agenda",
        "protect_route": "#/my-work/agenda/protect-today",
        "tag": "stackchain-deadline-digest-2026-08-13",
        "deadline_count": 1,
    }]
    assert "Private launch plan" not in json.dumps(sent)
    assert "private/repo" not in json.dumps(sent)


@pytest.mark.anyio
async def test_snoozed_deadline_revalidates_and_sends_once_after_expiry(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    store.set_deadline_preferences(
        "device-a", enabled=True, timezone="UTC", reminder_hour=9
    )
    store.mark_deadline_reminder_delivered("device-a", "2026-08-13")
    snoozed_at = datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc)
    store.snooze_deadline_reminder(
        "device-a", now=snoozed_at.timestamp(), delay_seconds=3_600
    )
    snapshots = 0
    sent = []

    async def assigned():
        nonlocal snapshots
        snapshots += 1
        return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-14"}]}

    async def send(_subscription, payload):
        sent.append(json.loads(payload))

    config = PushConfiguration("public", "private", "mailto:ops@example.com")
    assert await dispatch_deadline_reminders(
        store, config, assigned, send,
        now=datetime(2026, 8, 13, 10, 59, tzinfo=timezone.utc),
    ) == 0
    assert snapshots == 0

    assert await dispatch_deadline_reminders(
        store, config, assigned, send,
        now=datetime(2026, 8, 13, 11, 0, tzinfo=timezone.utc),
    ) == 1
    assert snapshots == 1
    assert sent[0]["deadline_count"] == 1
    assert sent[0]["tag"] == "stackchain-deadline-digest-2026-08-13"
    assert store.deadline_reminder_devices()[0].snoozed_until is None
    assert await dispatch_deadline_reminders(
        store, config, assigned, send,
        now=datetime(2026, 8, 13, 11, 1, tzinfo=timezone.utc),
    ) == 0
    assert snapshots == 1


@pytest.mark.anyio
async def test_expired_deadline_snooze_clears_when_no_deadlines_remain(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    store.set_deadline_preferences(
        "device-a", enabled=True, timezone="UTC", reminder_hour=9
    )
    store.mark_deadline_reminder_delivered("device-a", "2026-08-13")
    store.snooze_deadline_reminder(
        "device-a",
        now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc).timestamp(),
    )

    async def resolved():
        return {"complete": True, "items": []}

    assert await dispatch_deadline_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        resolved,
        now=datetime(2026, 8, 13, 11, 0, tzinfo=timezone.utc),
    ) == 0
    assert store.deadline_reminder_devices()[0].snoozed_until is None


@pytest.mark.anyio
async def test_expired_deadline_snooze_clears_when_deadlines_move_beyond_horizon(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    store.set_deadline_preferences(
        "device-a", enabled=True, timezone="UTC", reminder_hour=9, reminder_days=2
    )
    store.mark_deadline_reminder_delivered("device-a", "2026-08-13")
    store.snooze_deadline_reminder(
        "device-a",
        now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc).timestamp(),
    )

    async def replanned():
        return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-20"}]}

    assert await dispatch_deadline_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        replanned,
        now=datetime(2026, 8, 13, 11, 0, tzinfo=timezone.utc),
    ) == 0
    assert store.deadline_reminder_devices()[0].snoozed_until is None


@pytest.mark.anyio
async def test_deadline_reminder_counts_calendar_days_per_device_timezone(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for device, timezone_name in (("tokyo", "Asia/Tokyo"), ("la", "America/Los_Angeles")):
        store.upsert(device, {
            "endpoint": f"https://push.example/{device}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
        store.set_deadline_preferences(device, enabled=True, timezone=timezone_name, reminder_hour=0)

    async def assigned():
        return {"complete": True, "items": [
            {"id": 1, "due_date": "2026-08-15T23:59:59Z"},
            {"id": 2, "due_date": "2026-08-16T23:59:59Z"},
        ]}

    sent = {}
    async def send(subscription, payload):
        device = subscription["endpoint"].rsplit("/", 1)[-1]
        sent[device] = json.loads(payload)["deadline_count"]

    delivered = await dispatch_deadline_reminders(
        store, PushConfiguration("public", "private", "mailto:ops@example.com"),
        assigned, send, now=datetime(2026, 8, 13, 23, 30, tzinfo=timezone.utc),
    )

    assert delivered == 2
    assert sent == {"tokyo": 2, "la": 1}


@pytest.mark.anyio
async def test_deadline_reminder_uses_each_devices_confirmed_horizon(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for device, reminder_days in (("today", 0), ("week", 7)):
        store.upsert(device, {
            "endpoint": f"https://push.example/{device}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
        store.set_deadline_preferences(
            device,
            enabled=True,
            timezone="UTC",
            reminder_hour=9,
            reminder_days=reminder_days,
        )

    async def assigned():
        return {"complete": True, "items": [
            {"id": 1, "due_date": "2026-08-13"},
            {"id": 2, "due_date": "2026-08-15"},
            {"id": 3, "due_date": "2026-08-20"},
            {"id": 4, "due_date": "2026-08-21"},
        ]}

    sent = {}

    async def send(subscription, payload):
        device = subscription["endpoint"].rsplit("/", 1)[-1]
        sent[device] = json.loads(payload)["deadline_count"]

    delivered = await dispatch_deadline_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        assigned,
        send,
        now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc),
    )

    assert delivered == 2
    assert sent == {"today": 1, "week": 3}


@pytest.mark.anyio
async def test_deadline_reminder_fails_closed_for_incomplete_snapshot_and_before_local_hour(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    store.set_deadline_preferences(
        "device-a", enabled=True, timezone="America/Los_Angeles", reminder_hour=9
    )
    sent = []

    async def incomplete():
        return {"complete": False, "items": [{"id": 42, "due_date": "2026-08-14T12:00:00Z"}]}

    async def complete():
        return {"complete": True, "items": [{"id": 42, "due_date": "2026-08-14T12:00:00Z"}]}

    async def send(_subscription, payload):
        sent.append(payload)

    config = PushConfiguration("public", "private", "mailto:ops@example.com")
    assert await dispatch_deadline_reminders(
        store, config, incomplete, send,
        now=datetime(2026, 8, 13, 18, 0, tzinfo=timezone.utc),
    ) == 0
    assert await dispatch_deadline_reminders(
        store, config, complete, send,
        now=datetime(2026, 8, 13, 15, 0, tzinfo=timezone.utc),
    ) == 0
    assert sent == []


@pytest.mark.anyio
async def test_deadline_reminder_skips_snapshot_until_a_device_reaches_its_local_hour(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    store.set_deadline_preferences(
        "device-a", enabled=True, timezone="America/Los_Angeles", reminder_hour=9
    )
    snapshot_calls = 0

    async def assigned():
        nonlocal snapshot_calls
        snapshot_calls += 1
        return {"complete": True, "items": []}

    delivered = await dispatch_deadline_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        assigned,
        now=datetime(2026, 8, 13, 15, 0, tzinfo=timezone.utc),
    )

    assert delivered == 0
    assert snapshot_calls == 0


@pytest.mark.anyio
async def test_deadline_reminder_skips_snapshot_after_every_device_was_delivered_today(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for session_id, timezone_name in (
        ("device-a", "UTC"),
        ("device-b", "America/New_York"),
    ):
        store.upsert(session_id, {
            "endpoint": f"https://push.example/{session_id}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
        store.set_deadline_preferences(
            session_id, enabled=True, timezone=timezone_name, reminder_hour=9
        )
        store.mark_deadline_reminder_delivered(session_id, "2026-08-13")
    snapshot_calls = 0

    async def assigned():
        nonlocal snapshot_calls
        snapshot_calls += 1
        return {"complete": True, "items": []}

    delivered = await dispatch_deadline_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        assigned,
        now=datetime(2026, 8, 13, 15, 0, tzinfo=timezone.utc),
    )

    assert delivered == 0
    assert snapshot_calls == 0


def test_deadline_preferences_persist_on_the_existing_device_subscription(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })

    store.set_deadline_preferences(
        "device-a", enabled=True, timezone="Europe/London", reminder_hour=8
    )

    assert store.deadline_preferences("device-a") == {
        "enabled": True,
        "timezone": "Europe/London",
        "reminder_hour": 8,
        "reminder_days": 2,
        "snoozed_until": None,
    }


def test_deadline_snooze_is_device_bound_and_requires_enabled_reminders(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for session_id in ("enabled", "disabled"):
        store.upsert(session_id, {
            "endpoint": f"https://push.example/{session_id}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
    store.set_deadline_preferences(
        "enabled", enabled=True, timezone="UTC", reminder_hour=9
    )
    store.set_deadline_preferences(
        "disabled", enabled=False, timezone="UTC", reminder_hour=9
    )

    assert store.snooze_deadline_reminder(
        "enabled", now=1_765_000_000, delay_seconds=3_600
    ) is True
    assert store.snooze_deadline_reminder(
        "disabled", now=1_765_000_000, delay_seconds=3_600
    ) is False
    assert store.snooze_deadline_reminder(
        "missing", now=1_765_000_000, delay_seconds=3_600
    ) is False

    devices = {device.session_id: device for device in store.deadline_reminder_devices()}
    assert devices["enabled"].snoozed_until == 1_765_003_600
    assert "disabled" not in devices


def test_existing_deadline_preferences_migrate_to_two_day_horizon(tmp_path):
    database = tmp_path / "push.sqlite3"
    import sqlite3

    with sqlite3.connect(database) as connection:
        connection.executescript("""
            CREATE TABLE push_subscriptions (
                session_id TEXT PRIMARY KEY,
                endpoint TEXT NOT NULL UNIQUE,
                subscription_json TEXT NOT NULL
            );
            CREATE TABLE push_deadline_preferences (
                session_id TEXT PRIMARY KEY,
                enabled INTEGER NOT NULL DEFAULT 0,
                timezone TEXT NOT NULL DEFAULT 'UTC',
                reminder_hour INTEGER NOT NULL DEFAULT 9,
                delivered_local_day TEXT
            );
            INSERT INTO push_subscriptions VALUES (
                'device-a',
                'https://push.example/a',
                '{"endpoint":"https://push.example/a","keys":{"p256dh":"key","auth":"secret"}}'
            );
            INSERT INTO push_deadline_preferences VALUES ('device-a', 1, 'UTC', 8, NULL);
        """)

    store = PushSubscriptionStore(database)

    assert store.deadline_preferences("device-a")["reminder_days"] == 2


@pytest.mark.anyio
async def test_assigned_deadline_snapshot_is_pagination_complete(monkeypatch):
    pages = {
        1: {"items": [{"id": 1}], "total": 2, "has_more": True},
        2: {"items": [{"id": 2}], "total": 2, "has_more": False},
    }

    async def work_page(stream, page, limit):
        assert stream == "issue"
        assert limit == 1
        return pages[page]

    monkeypatch.setattr(gitea_proxy, "work_page", work_page)

    assert await gitea_proxy.assigned_issue_snapshot(limit=1) == {
        "items": [{"id": 1}, {"id": 2}],
        "complete": True,
    }


@pytest.mark.anyio
async def test_assigned_deadline_snapshot_fetches_remaining_pages_concurrently(monkeypatch):
    active = 0
    peak = 0
    remaining_started = asyncio.Event()

    async def work_page(stream, page, limit):
        nonlocal active, peak
        assert stream == "issue"
        assert limit == 1
        if page == 1:
            return {"items": [{"id": 1}], "total": 5, "has_more": True}
        active += 1
        peak = max(peak, active)
        if active == 2:
            remaining_started.set()
        await asyncio.wait_for(remaining_started.wait(), timeout=0.2)
        await asyncio.sleep(0)
        active -= 1
        return {"items": [{"id": page}], "total": 5, "has_more": page < 5}

    monkeypatch.setattr(gitea_proxy, "work_page", work_page)

    assert await gitea_proxy.assigned_issue_snapshot(limit=1, max_concurrency=2) == {
        "items": [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}],
        "complete": True,
    }
    assert peak == 2


@pytest.mark.anyio
@pytest.mark.parametrize(
    "remaining",
    [
        {"items": [{"id": 2}], "total": 3, "has_more": True},
        {"items": [{"id": 1}], "total": 2, "has_more": False},
    ],
)
async def test_assigned_deadline_snapshot_rejects_changed_or_duplicate_pages(
    monkeypatch, remaining
):
    async def work_page(_stream, page, _limit):
        if page == 1:
            return {"items": [{"id": 1}], "total": 2, "has_more": True}
        return remaining

    monkeypatch.setattr(gitea_proxy, "work_page", work_page)

    with pytest.raises(ValueError, match="incomplete|changed"):
        await gitea_proxy.assigned_issue_snapshot(limit=1)


@pytest.mark.anyio
async def test_assigned_deadline_snapshot_enforces_aggregate_deadline(monkeypatch):
    cancelled = asyncio.Event()

    async def work_page(_stream, _page, _limit):
        try:
            await asyncio.sleep(60)
        finally:
            cancelled.set()

    monkeypatch.setattr(gitea_proxy, "work_page", work_page)

    with pytest.raises(TimeoutError):
        await gitea_proxy.assigned_issue_snapshot(deadline_seconds=0.01)
    assert cancelled.is_set()


@pytest.mark.anyio
async def test_competing_workers_send_one_deadline_digest(tmp_path):
    path = tmp_path / "push.sqlite3"
    first = PushSubscriptionStore(path)
    second = PushSubscriptionStore(path)
    first.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    first.set_deadline_preferences(
        "device-a", enabled=True, timezone="UTC", reminder_hour=9
    )
    sending = __import__("asyncio").Event()
    release = __import__("asyncio").Event()
    sent = []

    async def assigned():
        return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-14T00:00:00Z"}]}

    async def send(_subscription, payload):
        sent.append(payload)
        sending.set()
        await release.wait()

    config = PushConfiguration("public", "private", "mailto:ops@example.com")
    now = datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc)
    active = __import__("asyncio").create_task(
        dispatch_deadline_reminders(first, config, assigned, send, now=now)
    )
    await sending.wait()
    competing = await dispatch_deadline_reminders(second, config, assigned, send, now=now)
    release.set()

    assert competing == 0
    assert await active == 1
    assert len(sent) == 1


@pytest.mark.anyio
async def test_deadline_reminders_bound_fanout_and_isolate_failed_devices(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for index in range(4):
        session_id = f"device-{index}"
        store.upsert(session_id, {
            "endpoint": f"https://push.example/{session_id}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
        store.set_deadline_preferences(
            session_id, enabled=True, timezone="UTC", reminder_hour=9
        )
    active = 0
    peak = 0
    two_active = asyncio.Event()

    async def assigned():
        return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-14T00:00:00Z"}]}

    async def send(subscription, _payload):
        nonlocal active, peak
        active += 1
        peak = max(peak, active)
        if active == 2:
            two_active.set()
        await asyncio.wait_for(two_active.wait(), timeout=0.5)
        active -= 1
        if subscription["endpoint"].endswith("device-1"):
            raise RuntimeError("provider failed")

    delivered = await dispatch_deadline_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        assigned,
        send,
        now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc),
        max_concurrency=2,
    )

    assert delivered == 3
    assert peak == 2


@pytest.mark.anyio
async def test_deadline_reminders_bulk_authorize_due_devices_and_prune_inactive(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for session_id in ("active-device", "revoked-device"):
        store.upsert(session_id, {
            "endpoint": f"https://push.example/{session_id}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
        store.set_deadline_preferences(
            session_id, enabled=True, timezone="UTC", reminder_hour=9
        )
    authorization_calls = []
    sent = []

    async def assigned():
        return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-14T00:00:00Z"}]}

    async def session_statuses(management_ids):
        authorization_calls.append(list(management_ids))
        return {
            management_id: "active" if management_id == "active-device" else "revoked"
            for management_id in management_ids
        }

    async def send(subscription, _payload):
        sent.append(subscription["endpoint"])

    delivered = await dispatch_deadline_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        assigned,
        send,
        now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc),
        session_statuses=session_statuses,
    )

    assert delivered == 1
    assert authorization_calls == [["active-device", "revoked-device"]]
    assert sent == ["https://push.example/active-device"]
    assert store.is_subscribed("revoked-device") is False
