import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from urllib.parse import parse_qs, urlsplit

from aiogram.exceptions import TelegramBadRequest
from aiogram.methods import SendMessage

from bot.services import subscription_lifecycle_notifications as lifecycle
from bot.services.subscription_lifecycle_notifications import (
    SubscriptionLifecycleNotificationService,
    SubscriptionNotificationStage,
)


class FakeI18n:
    def gettext(self, lang_code, key, **kwargs):
        messages = {
            "subscription_72h_notification": "Hi {user_name}, expires on {end_date}",
            "email_subscription_lifecycle_subject_before_days": "{days} days left",
            "email_subscription_lifecycle_subject_before_hours": "{hours} hours left",
            "email_subscription_lifecycle_subject_expired": "Expired",
            "email_subscription_lifecycle_subject_expired_after": "Expired yesterday",
            "email_subscription_lifecycle_subject_autorenew": "Auto-renewal tomorrow",
            "email_subscription_lifecycle_intro": "Subscription notice",
            "email_subscription_lifecycle_intro_direct": "Direct email notice",
            "email_subscription_lifecycle_intro_mirrored": "Mirrored Telegram notice",
            "email_subscription_lifecycle_row_end_date": "Active until",
            "email_subscription_lifecycle_cta": "Open dashboard",
            "email_subscription_lifecycle_text_renew": "Dashboard: {url}",
            "email_footer_auto": "Sent by {brand}",
        }
        return messages.get(key, key).format(**kwargs)


class FakeBot:
    def __init__(self):
        self.messages = []

    async def send_message(self, chat_id, text, reply_markup=None):
        self.messages.append(
            {
                "chat_id": chat_id,
                "text": text,
                "reply_markup": reply_markup,
            }
        )


class ChatNotFoundBot:
    def __init__(self):
        self.calls = []

    async def send_message(self, chat_id, text, reply_markup=None):
        self.calls.append((chat_id, text, reply_markup))
        raise TelegramBadRequest(
            method=SendMessage(chat_id=chat_id, text=text),
            message="Bad Request: chat not found",
        )


class FakeEmailService:
    def __init__(self):
        self.messages = []

    async def send_rendered_email(self, *, email, content):
        self.messages.append({"email": email, "content": content})


def _settings(**overrides):
    return SimpleNamespace(
        DEFAULT_LANGUAGE="ru",
        SUBSCRIPTION_EMAIL_NOTIFICATIONS_ENABLED=True,
        SUBSCRIPTION_MINI_APP_URL="https://app.example.test/",
        WEBAPP_PRIMARY_COLOR="#00fe7a",
        WEBAPP_TITLE="Minishop",
        WEBAPP_LOGO_USE_EMOJI=False,
        WEBAPP_LOGO_URL="",
        email_auth_configured=True,
        **overrides,
    )


def _subscription(**overrides):
    data = {
        "subscription_id": 42,
        "user_id": 123,
        "end_date": datetime(2026, 6, 1, tzinfo=timezone.utc),
        "tariff_key": "standard",
        "provider": "yookassa",
        "status_from_panel": "ACTIVE",
    }
    data.update(overrides)
    return SimpleNamespace(**data)


def _user(**overrides):
    data = {
        "user_id": 123,
        "telegram_id": 555,
        "email": "user@example.test",
        "language_code": "ru",
        "first_name": "Ada",
    }
    data.update(overrides)
    return SimpleNamespace(**data)


def test_send_stage_records_telegram_and_email_channel_keys(monkeypatch):
    recorded = []
    status_changes = []

    async def fake_has(session, subscription_id, notification_key):
        return notification_key in recorded

    async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
        recorded.append(notification_key)

    async def fake_mark_status(session, user_id, status, *, telegram_id=None, checked_at=None):
        status_changes.append(
            {
                "user_id": user_id,
                "status": status,
                "telegram_id": telegram_id,
                "checked_at": checked_at,
            }
        )

    monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
    monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
    monkeypatch.setattr(lifecycle, "mark_telegram_notifications_status", fake_mark_status)

    bot = FakeBot()
    email_service = FakeEmailService()
    service = SubscriptionLifecycleNotificationService(
        _settings(),
        bot,
        FakeI18n(),
        email_service=email_service,
    )

    async def run():
        return await service.send_stage(
            object(),
            _subscription(),
            SubscriptionNotificationStage(
                key="before_3d",
                message_key="subscription_72h_notification",
                days_left=3,
            ),
            user=_user(),
            telegram_markup="markup",
        )

    delivery = asyncio.run(run())

    assert delivery.telegram_sent is True
    assert delivery.email_sent is True
    assert bot.messages == [
        {
            "chat_id": 555,
            "text": "Hi Ada, expires on 2026-06-01",
            "reply_markup": "markup",
        }
    ]
    assert email_service.messages[0]["email"] == "user@example.test"
    assert "Mirrored Telegram notice" in email_service.messages[0]["content"].html
    assert recorded == ["before_3d:telegram", "before_3d:email"]
    assert status_changes == [
        {
            "user_id": 123,
            "status": lifecycle.TELEGRAM_NOTIFICATIONS_ENABLED,
            "telegram_id": 555,
            "checked_at": status_changes[0]["checked_at"],
        }
    ]
    assert status_changes[0]["checked_at"] is not None


def test_legacy_stage_key_suppresses_only_telegram(monkeypatch):
    recorded = ["before_3d"]

    async def fake_has(session, subscription_id, notification_key):
        return notification_key in recorded

    async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
        recorded.append(notification_key)

    monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
    monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)

    bot = FakeBot()
    email_service = FakeEmailService()
    service = SubscriptionLifecycleNotificationService(
        _settings(),
        bot,
        FakeI18n(),
        email_service=email_service,
    )

    async def run():
        return await service.send_stage(
            object(),
            _subscription(),
            SubscriptionNotificationStage(
                key="before_3d",
                message_key="subscription_72h_notification",
                days_left=3,
            ),
            user=_user(),
            telegram_markup="markup",
        )

    delivery = asyncio.run(run())

    assert delivery.telegram_sent is False
    assert delivery.email_sent is True
    assert bot.messages == []
    assert email_service.messages[0]["email"] == "user@example.test"
    assert recorded == ["before_3d", "before_3d:email"]


def test_unstarted_telegram_failure_marks_status_without_recording_delivery(monkeypatch):
    recorded = []
    status_changes = []

    async def fake_has(session, subscription_id, notification_key):
        return notification_key in recorded

    async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
        recorded.append(notification_key)

    async def fake_mark_status(session, user_id, status, *, telegram_id=None, checked_at=None):
        status_changes.append(
            {
                "user_id": user_id,
                "status": status,
                "telegram_id": telegram_id,
                "checked_at": checked_at,
            }
        )

    monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
    monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
    monkeypatch.setattr(lifecycle, "mark_telegram_notifications_status", fake_mark_status)

    bot = ChatNotFoundBot()
    settings = _settings()
    settings.email_auth_configured = False
    service = SubscriptionLifecycleNotificationService(
        settings,
        bot,
        FakeI18n(),
    )
    user = _user()
    user.telegram_id = 777
    user.email = ""

    async def run():
        return await service.send_stage(
            object(),
            _subscription(),
            SubscriptionNotificationStage(
                key="before_3d",
                message_key="subscription_72h_notification",
                days_left=3,
            ),
            user=user,
            telegram_markup="markup",
        )

    delivery = asyncio.run(run())

    assert delivery.telegram_sent is False
    assert delivery.email_sent is False
    assert bot.calls[0][0] == 777
    assert recorded == []
    assert status_changes == [
        {
            "user_id": 123,
            "status": lifecycle.TELEGRAM_NOTIFICATIONS_NEEDS_START,
            "telegram_id": None,
            "checked_at": None,
        }
    ]


def test_email_only_user_gets_email_name_direct_copy_and_renewal_login_link(monkeypatch):
    recorded = []

    async def fake_has(session, subscription_id, notification_key):
        return notification_key in recorded

    async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
        recorded.append(notification_key)

    monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
    monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)

    bot = FakeBot()
    email_service = FakeEmailService()
    service = SubscriptionLifecycleNotificationService(
        _settings(),
        bot,
        FakeI18n(),
        email_service=email_service,
    )
    user = _user(user_id=-8758169927032035, telegram_id=None, first_name=None)

    async def run():
        return await service.send_stage(
            object(),
            _subscription(user_id=user.user_id),
            SubscriptionNotificationStage(
                key="expired",
                message_key="subscription_72h_notification",
                days_left=0,
            ),
            user=user,
            telegram_markup="markup",
        )

    delivery = asyncio.run(run())

    assert delivery.telegram_sent is False
    assert delivery.email_sent is True
    assert bot.messages == []
    content = email_service.messages[0]["content"]
    assert "Hi user@example.test, expires on 2026-06-01" in content.text
    assert "User -8758169927032035" not in content.text
    assert "Direct email notice" in content.html
    assert "Mirrored Telegram notice" not in content.html

    dashboard_line = next(
        line for line in content.text.splitlines() if line.startswith("Dashboard: ")
    )
    url = dashboard_line.removeprefix("Dashboard: ")
    parsed = urlsplit(url)
    query = parse_qs(parsed.query)
    assert parsed.scheme == "https"
    assert parsed.netloc == "app.example.test"
    assert query["login"] == ["email_code"]
    assert query["login_email"] == ["user@example.test"]
    assert query["after_login"] == ["renew"]
    assert query["renew"] == ["1"]
    assert query["renew_tariff"] == ["standard"]
    assert recorded == ["expired:email"]


def test_trial_subscription_renewal_link_omits_tariff_for_default_fallback(monkeypatch):
    recorded = []

    async def fake_has(session, subscription_id, notification_key):
        return notification_key in recorded

    async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
        recorded.append(notification_key)

    monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
    monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)

    email_service = FakeEmailService()
    service = SubscriptionLifecycleNotificationService(
        _settings(),
        FakeBot(),
        FakeI18n(),
        email_service=email_service,
    )
    user = _user(user_id=-1001, telegram_id=None, first_name=None)

    async def run():
        return await service.send_stage(
            object(),
            _subscription(
                user_id=user.user_id,
                tariff_key="trial-tariff",
                provider="trial",
                status_from_panel="TRIAL",
            ),
            SubscriptionNotificationStage(
                key="expired",
                message_key="subscription_72h_notification",
                days_left=0,
            ),
            user=user,
        )

    asyncio.run(run())

    content = email_service.messages[0]["content"]
    dashboard_line = next(
        line for line in content.text.splitlines() if line.startswith("Dashboard: ")
    )
    query = parse_qs(urlsplit(dashboard_line.removeprefix("Dashboard: ")).query)
    assert "renew_tariff" not in query
