import asyncio
import json
from datetime import datetime, timezone
from pathlib import Path

import pytest
from types import SimpleNamespace

from src import main
from src.push_notifications import PushConfiguration, dispatch_start_day_reminders
from src.push_subscription_store import PushSubscriptionStore


@pytest.mark.anyio
async def test_start_day_reminder_sends_one_private_prompt_for_due_plan(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_start_day_preferences(
        "device-a", enabled=True, timezone="America/New_York", reminder_hour=9
    )
    sent = []

    async def tomorrow():
        return {
            "revision": 3,
            "ids": ["private/repo#42"],
            "plan_date": "2026-08-20",
            "timezone": "America/New_York",
        }

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

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

    assert await dispatch_start_day_reminders(
        store, configuration, tomorrow, send, now=now
    ) == 1
    assert await dispatch_start_day_reminders(
        store, configuration, tomorrow, send, now=now
    ) == 0
    assert sent == [{
        "title": "Your planned day is ready",
        "body": "Open Stackchain to prepare Today.",
        "route": "#/my-work/start-day",
        "tag": "stackchain-start-day-2026-08-20",
        "plan_date": "2026-08-20",
    }]
    assert "private/repo" not in json.dumps(sent)


@pytest.mark.anyio
async def test_start_day_reminders_bound_parallel_device_delivery(tmp_path):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for index in range(6):
        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_start_day_preferences(
            session_id, enabled=True, timezone="UTC", reminder_hour=9
        )

    active = 0
    peak = 0
    first_wave = asyncio.Event()
    release = asyncio.Event()

    async def tomorrow():
        return {"ids": ["one"], "plan_date": "2026-08-20"}

    async def send(_subscription, _payload):
        nonlocal active, peak
        active += 1
        peak = max(peak, active)
        if active == 2:
            first_wave.set()
        await release.wait()
        active -= 1

    dispatch = asyncio.create_task(dispatch_start_day_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        tomorrow,
        send,
        now=datetime(2026, 8, 20, 9, 0, tzinfo=timezone.utc),
        max_concurrency=2,
    ))
    await asyncio.wait_for(first_wave.wait(), timeout=0.5)
    assert peak == 2
    release.set()
    assert await dispatch == 6
    assert peak == 2


@pytest.mark.anyio
async def test_start_day_reminder_stops_queued_send_after_lease_loss(
    tmp_path, monkeypatch
):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for session_id in ("device-a", "device-b"):
        store.upsert(session_id, {
            "endpoint": f"https://push.example/{session_id}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
        store.set_start_day_preferences(
            session_id, enabled=True, timezone="UTC", reminder_hour=9
        )

    lease_checks = 0

    def acquire(_owner, *, channel, now, lease_seconds):
        nonlocal lease_checks
        assert channel == "start-day"
        assert now > 0
        assert lease_seconds >= 15
        lease_checks += 1
        return lease_checks <= 2

    monkeypatch.setattr(store, "acquire_dispatch_lease", acquire)
    sent = []

    async def tomorrow():
        return {"ids": ["one"], "plan_date": "2026-08-20"}

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

    delivered = await dispatch_start_day_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        tomorrow,
        send,
        now=datetime(2026, 8, 20, 9, 0, tzinfo=timezone.utc),
        max_concurrency=1,
    )

    assert delivered == 1
    assert lease_checks == 3
    assert len(sent) == 1


@pytest.mark.anyio
async def test_start_day_reminder_contains_one_device_persistence_failure(
    tmp_path, monkeypatch
):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    for session_id in ("device-a", "device-b"):
        store.upsert(session_id, {
            "endpoint": f"https://push.example/{session_id}",
            "keys": {"p256dh": "public-key", "auth": "auth-secret"},
        })
        store.set_start_day_preferences(
            session_id, enabled=True, timezone="UTC", reminder_hour=9
        )

    mark_succeeded = store.mark_delivery_succeeded

    def fail_one_device(session_id, channel):
        if session_id == "device-a":
            raise RuntimeError("device checkpoint unavailable")
        return mark_succeeded(session_id, channel)

    monkeypatch.setattr(store, "mark_delivery_succeeded", fail_one_device)
    sent = []

    async def tomorrow():
        return {"ids": ["one"], "plan_date": "2026-08-20"}

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

    delivered = await dispatch_start_day_reminders(
        store,
        PushConfiguration("public", "private", "mailto:ops@example.com"),
        tomorrow,
        send,
        now=datetime(2026, 8, 20, 9, 0, tzinfo=timezone.utc),
        max_concurrency=2,
    )

    assert delivered == 1
    assert len(sent) == 2


@pytest.mark.anyio
async def test_push_poll_applies_configured_concurrency_to_start_day(
    monkeypatch
):
    captured = {}
    hold = asyncio.Event()

    async def no_wait(_seconds):
        return None

    async def hold_dispatch(*_args, **_kwargs):
        await hold.wait()

    async def capture_start_day(*_args, **kwargs):
        captured.update(kwargs)
        raise asyncio.CancelledError

    monkeypatch.setenv("STACKCHAIN_PUSH_MAX_CONCURRENCY", "3")
    monkeypatch.setattr(main.asyncio, "sleep", no_wait)
    monkeypatch.setattr(main, "dispatch_unread_updates", hold_dispatch)
    monkeypatch.setattr(main, "dispatch_deadline_reminders", hold_dispatch)
    monkeypatch.setattr(main, "dispatch_start_day_reminders", capture_start_day)

    with pytest.raises(asyncio.CancelledError):
        await main._push_poll_loop()

    assert captured["max_concurrency"] == 3


@pytest.mark.anyio
async def test_start_day_reminder_waits_for_hour_and_nonempty_due_plan(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_start_day_preferences(
        "device-a", enabled=True, timezone="America/Los_Angeles", reminder_hour=9
    )
    sent = []

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

    configuration = PushConfiguration("public", "private", "mailto:ops@example.com")
    before_hour = datetime(2026, 8, 20, 15, 59, tzinfo=timezone.utc)

    async def due_plan():
        return {"ids": ["one"], "plan_date": "2026-08-20"}

    assert await dispatch_start_day_reminders(
        store, configuration, due_plan, send, now=before_hour
    ) == 0

    async def empty_plan():
        return {"ids": [], "plan_date": "2026-08-20"}

    assert await dispatch_start_day_reminders(
        store, configuration, empty_plan, send,
        now=datetime(2026, 8, 20, 16, 0, tzinfo=timezone.utc),
    ) == 0
    assert sent == []


@pytest.mark.anyio
async def test_authenticated_device_can_enable_start_day_reminders(tmp_path, monkeypatch):
    store = PushSubscriptionStore(tmp_path / "push.sqlite3")
    store.upsert("device-a", {
        "endpoint": "https://push.example/device-a",
        "keys": {"p256dh": "public-key", "auth": "auth-secret"},
    })
    monkeypatch.setattr(main, "_push_subscription_store", store)

    async def management_id(_session):
        return "device-a"

    monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id)
    request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object()))
    payload = main.StartDayReminderPayload(
        enabled=True, timezone="America/New_York", reminder_hour=8
    )

    assert await main.update_start_day_reminders(payload, request) == {
        "start_day_enabled": True,
        "start_day_timezone": "America/New_York",
        "start_day_reminder_hour": 8,
    }
    assert store.start_day_preferences("device-a") == {
        "enabled": True, "timezone": "America/New_York", "reminder_hour": 8,
    }


@pytest.mark.anyio
async def test_worker_snapshot_reads_the_confirmed_accounts_tomorrow_plan(monkeypatch):
    async def current_user():
        return {"login": "Timmy"}

    class Store:
        def get_tomorrow(self, login):
            assert login == "Timmy"
            return {"ids": ["one"], "plan_date": "2026-08-20"}

    monkeypatch.setattr(main, "current_user", current_user)
    monkeypatch.setattr(main, "_today_store", lambda: Store())

    assert await main._start_day_plan_snapshot() == {
        "ids": ["one"], "plan_date": "2026-08-20",
    }


def test_mobile_settings_and_launch_route_wire_start_day_into_existing_promotion():
    frontend = Path(__file__).parents[1] / "frontend"
    html = (frontend / "index.html").read_text()
    dashboard = (frontend / "dashboard.js").read_text()

    assert 'id="push-start-day"' in html
    assert 'id="push-start-day-hour"' in html
    assert 'id="device-setup-start-day"' in html
    assert "startDayControl:qs('#push-start-day')" in dashboard
    assert "startDayHour:qs('#push-start-day-hour')" in dashboard
    assert "window.location.hash === '#/my-work/start-day'" in dashboard
    assert "weekWorkflow.promote(plan).finally(() =>" in dashboard
    assert "onRemotePlan: async plan =>" not in dashboard
    assert "openMobileStartDay()" in dashboard
