import asyncio
import json
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace

from bot.payment_providers import wata
from bot.payment_providers.wata import WataConfig, WataService


class _FakeRequest:
    def __init__(self, payload):
        self.headers = {}
        self.remote = "127.0.0.1"
        self._raw_body = json.dumps(payload).encode("utf-8")

    async def read(self):
        return self._raw_body


class _FakeSession:
    def __init__(self):
        self.commits = 0
        self.rollbacks = 0

    def __call__(self):
        return self

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return False

    async def commit(self):
        self.commits += 1

    async def rollback(self):
        self.rollbacks += 1


def _payment(**overrides):
    values = {
        "payment_id": 465,
        "user_id": 748116183,
        "status": "pending_wata",
        "amount": 100.0,
        "provider_payment_id": "link-id",
        "purchased_gb": None,
        "purchased_hwid_devices": None,
        "subscription_duration_months": 1,
        "sale_mode": "subscription",
        "user": None,
        "created_at": datetime.now(timezone.utc),
    }
    values.update(overrides)
    return SimpleNamespace(**values)


def _service(session, **config_overrides):
    settings = SimpleNamespace(
        DEFAULT_CURRENCY_SYMBOL="RUB",
        DEFAULT_LANGUAGE="ru",
        traffic_sale_mode=False,
        trusted_proxies=[],
    )
    config_values = {
        "ENABLED": True,
        "API_TOKEN": "token",
        "WEBHOOK_VERIFY_SIGNATURE": False,
        "TRUSTED_IPS": "",
    }
    config_values.update(config_overrides)
    return WataService(
        bot=SimpleNamespace(),
        settings=settings,
        config=WataConfig(**config_values),
        i18n=SimpleNamespace(),
        async_session_factory=session,
        subscription_service=SimpleNamespace(),
        referral_service=SimpleNamespace(),
        default_return_url="test_bot",
    )


def test_wata_created_webhook_returns_ok_and_persists_transaction_id(monkeypatch):
    session = _FakeSession()
    payment = _payment()
    updates = []

    async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
        assert _session is session
        assert order_id_raw == "465"
        assert provider_payment_id == "tx-1"
        return payment

    async def update_provider_payment_and_status(
        _session,
        payment_id,
        provider_payment_id,
        status,
    ):
        updates.append((payment_id, provider_payment_id, status))

    monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
    monkeypatch.setattr(
        wata.payment_dal,
        "update_provider_payment_and_status",
        update_provider_payment_and_status,
    )

    response = asyncio.run(
        _service(session).webhook_route(
            _FakeRequest(
                {
                    "transactionStatus": "Created",
                    "transactionId": "tx-1",
                    "orderId": "465",
                    "amount": 100,
                    "currency": "RUB",
                }
            )
        )
    )

    assert response.status == 200
    assert updates == [(465, "tx-1", "pending_wata")]
    assert session.commits == 1
    assert session.rollbacks == 0


def test_wata_created_webhook_can_find_payment_by_payment_link_id(monkeypatch):
    session = _FakeSession()
    payment = _payment()
    lookup_calls = []
    updates = []

    async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
        lookup_calls.append((order_id_raw, provider_payment_id))
        if provider_payment_id == "link-id":
            return payment
        return None

    async def update_provider_payment_and_status(
        _session,
        payment_id,
        provider_payment_id,
        status,
    ):
        updates.append((payment_id, provider_payment_id, status))

    monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
    monkeypatch.setattr(
        wata.payment_dal,
        "update_provider_payment_and_status",
        update_provider_payment_and_status,
    )

    response = asyncio.run(
        _service(session).webhook_route(
            _FakeRequest(
                {
                    "transactionStatus": "Created",
                    "transactionId": "tx-1",
                    "paymentLinkId": "link-id",
                    "amount": 100,
                    "currency": "RUB",
                }
            )
        )
    )

    assert response.status == 200
    assert lookup_calls == [(None, "tx-1"), (None, "link-id")]
    assert updates == [(465, "tx-1", "pending_wata")]
    assert session.commits == 1


def test_wata_known_payment_with_unknown_status_still_acknowledges_webhook(monkeypatch):
    session = _FakeSession()
    payment = _payment(provider_payment_id="tx-1")

    async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
        return payment

    async def update_provider_payment_and_status(*args, **kwargs):
        raise AssertionError("unknown statuses must not mutate payment state")

    monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
    monkeypatch.setattr(
        wata.payment_dal,
        "update_provider_payment_and_status",
        update_provider_payment_and_status,
    )

    response = asyncio.run(
        _service(session).webhook_route(
            _FakeRequest(
                {
                    "transactionStatus": "WaitingForBank",
                    "transactionId": "tx-1",
                    "orderId": "465",
                }
            )
        )
    )

    assert response.status == 200
    assert session.commits == 0


def test_wata_refresh_finds_paid_transaction_by_order_id_and_finalizes(monkeypatch):
    session = _FakeSession()
    payment = _payment(provider="wata", provider_payment_id="link-id")
    updates = []
    finalized = []
    service = _service(session)

    async def search_transactions(*, order_id=None, payment_link_id=None, status=None, limit=5):
        assert order_id == "465"
        assert payment_link_id is None
        assert status == "Paid"
        assert limit == 5
        return True, {
            "items": [
                {
                    "id": "tx-paid",
                    "status": "Paid",
                    "orderId": "465",
                    "amount": 100,
                    "currency": "RUB",
                    "paymentLinkId": "link-id",
                }
            ]
        }

    async def get_payment_by_db_id(_session, payment_id):
        assert _session is session
        assert payment_id == 465
        return payment

    async def update_provider_payment_and_status(
        _session,
        payment_id,
        provider_payment_id,
        status,
    ):
        updates.append((payment_id, provider_payment_id, status))
        payment.provider_payment_id = provider_payment_id
        payment.status = status

    async def finalize_successful_payment(request):
        finalized.append(
            (
                request.payment.payment_id,
                request.provider_subscription,
                request.provider_notification,
            )
        )
        return SimpleNamespace()

    service.search_transactions = search_transactions
    monkeypatch.setattr(wata.payment_dal, "get_payment_by_db_id", get_payment_by_db_id)
    monkeypatch.setattr(
        wata.payment_dal,
        "update_provider_payment_and_status",
        update_provider_payment_and_status,
    )
    monkeypatch.setattr(wata, "finalize_successful_payment", finalize_successful_payment)

    result = asyncio.run(service.refresh_payment_status(session, payment))

    assert result is payment
    assert updates == [(465, "tx-paid", "succeeded")]
    assert finalized == [(465, "wata", "wata")]
    assert session.commits == 1


def test_wata_hwid_payment_finalizes_purchased_device_count(monkeypatch):
    session = _FakeSession()
    payment = _payment(
        provider="wata",
        provider_payment_id="link-id",
        sale_mode="hwid_devices@standard",
        subscription_duration_months=None,
        purchased_hwid_devices=3,
    )
    finalized = []
    service = _service(session)

    async def search_transactions(*, order_id=None, payment_link_id=None, status=None, limit=5):
        return True, {
            "items": [
                {
                    "id": "tx-paid",
                    "status": "Paid",
                    "orderId": "465",
                    "amount": 100,
                    "currency": "RUB",
                    "paymentLinkId": "link-id",
                }
            ]
        }

    async def get_payment_by_db_id(_session, payment_id):
        assert payment_id == 465
        return payment

    async def update_provider_payment_and_status(
        _session,
        payment_id,
        provider_payment_id,
        status,
    ):
        payment.provider_payment_id = provider_payment_id
        payment.status = status

    async def finalize_successful_payment(request):
        finalized.append((request.months, request.traffic_amount, request.sale_mode))
        return SimpleNamespace()

    service.search_transactions = search_transactions
    monkeypatch.setattr(wata.payment_dal, "get_payment_by_db_id", get_payment_by_db_id)
    monkeypatch.setattr(
        wata.payment_dal,
        "update_provider_payment_and_status",
        update_provider_payment_and_status,
    )
    monkeypatch.setattr(wata, "finalize_successful_payment", finalize_successful_payment)

    result = asyncio.run(service.refresh_payment_status(session, payment))

    assert result is payment
    assert finalized == [(3, 3.0, "hwid_devices@standard")]


def test_try_reuse_pending_link_returns_url_for_opened_link():
    service = _service(_FakeSession())
    payment = _payment(provider_payment_id="link-id")

    future_iso = "2099-01-01T00:00:00Z"

    async def fake_get_payment_link(payment_link_id):
        assert payment_link_id == "link-id"
        return True, {
            "id": "link-id",
            "status": "Opened",
            "url": "https://wata.pro/p/link-id",
            "expirationDateTime": future_iso,
        }

    service.get_payment_link = fake_get_payment_link

    url = asyncio.run(service.try_reuse_pending_link(payment))
    assert url == "https://wata.pro/p/link-id"


def test_try_reuse_pending_link_returns_none_for_closed_link():
    service = _service(_FakeSession())
    payment = _payment(provider_payment_id="link-id")

    async def fake_get_payment_link(payment_link_id):
        return True, {
            "id": "link-id",
            "status": "Closed",
            "url": "https://wata.pro/p/link-id",
            "expirationDateTime": "2099-01-01T00:00:00Z",
        }

    service.get_payment_link = fake_get_payment_link

    url = asyncio.run(service.try_reuse_pending_link(payment))
    assert url is None


def test_try_reuse_pending_link_returns_none_for_expired_link():
    service = _service(_FakeSession())
    payment = _payment(provider_payment_id="link-id")

    async def fake_get_payment_link(payment_link_id):
        return True, {
            "id": "link-id",
            "status": "Opened",
            "url": "https://wata.pro/p/link-id",
            "expirationDateTime": "2000-01-01T00:00:00Z",
        }

    service.get_payment_link = fake_get_payment_link

    url = asyncio.run(service.try_reuse_pending_link(payment))
    assert url is None


def test_try_reuse_pending_link_returns_none_when_no_provider_payment_id():
    service = _service(_FakeSession())
    payment = _payment(provider_payment_id=None)

    async def fake_get_payment_link(payment_link_id):
        raise AssertionError("get_payment_link must not be called without provider id")

    service.get_payment_link = fake_get_payment_link

    url = asyncio.run(service.try_reuse_pending_link(payment))
    assert url is None


def test_try_reuse_pending_link_returns_none_when_get_fails():
    service = _service(_FakeSession())
    payment = _payment(provider_payment_id="link-id")

    async def fake_get_payment_link(payment_link_id):
        return False, {"status": 429, "message": "rate_limited"}

    service.get_payment_link = fake_get_payment_link

    url = asyncio.run(service.try_reuse_pending_link(payment))
    assert url is None


def test_create_payment_link_uses_clean_iso_expiration_without_microseconds(monkeypatch):
    captured = {}

    async def fake_post_json_request(session, url, *, body, headers, log_prefix, is_success):
        captured["body"] = body
        return True, {"id": "link-1", "url": "https://wata.pro/p/link-1"}

    monkeypatch.setattr(wata, "post_json_request", fake_post_json_request)

    service = _service(_FakeSession())

    success, _ = asyncio.run(
        service.create_payment_link(
            payment_db_id=465,
            amount=199.5,
            currency="RUB",
            description="Оплата подписки на 1 мес.",
        )
    )
    assert success is True

    expiration = captured["body"]["expirationDateTime"]
    assert expiration.endswith("Z"), expiration
    assert "." not in expiration, expiration
    assert "+" not in expiration, expiration
    expiration_dt = wata._parse_wata_datetime(expiration)
    assert expiration_dt is not None
    ttl_delta = expiration_dt - datetime.now(timezone.utc)
    assert timedelta(minutes=14, seconds=30) <= ttl_delta <= timedelta(minutes=15, seconds=30)


def test_wata_link_ttl_uses_minutes_with_default_and_minimum():
    default_config = WataConfig(ENABLED=True, API_TOKEN="token")
    assert default_config.LINK_TTL_MINUTES == 15

    too_short_config = WataConfig(ENABLED=True, API_TOKEN="token", LINK_TTL_MINUTES=10)
    assert too_short_config.LINK_TTL_MINUTES == 15

    custom_config = WataConfig(ENABLED=True, API_TOKEN="token", LINK_TTL_MINUTES=45)
    assert custom_config.LINK_TTL_MINUTES == 45


def test_refresh_marks_expired_wata_link_as_canceled(monkeypatch):
    session = _FakeSession()
    service = _service(session)
    payment = _payment(
        provider="wata",
        provider_payment_id="link-id",
        created_at=datetime.now(timezone.utc) - timedelta(minutes=16),
    )
    updates = []

    async def find_transaction_for_payment(_payment, *, status):
        return None

    async def get_payment_link(payment_link_id):
        assert payment_link_id == "link-id"
        return True, {
            "id": "link-id",
            "status": "Opened",
            "expirationDateTime": "2000-01-01T00:00:00Z",
        }

    async def update_provider_payment_and_status(
        _session,
        payment_id,
        provider_payment_id,
        status,
    ):
        updates.append((payment_id, provider_payment_id, status))
        payment.provider_payment_id = provider_payment_id
        payment.status = status

    async def get_payment_by_db_id(_session, payment_id):
        assert payment_id == payment.payment_id
        return payment

    service._find_transaction_for_payment = find_transaction_for_payment
    service.get_payment_link = get_payment_link
    monkeypatch.setattr(
        wata.payment_dal,
        "update_provider_payment_and_status",
        update_provider_payment_and_status,
    )
    monkeypatch.setattr(wata.payment_dal, "get_payment_by_db_id", get_payment_by_db_id)

    result = asyncio.run(service.refresh_payment_status(session, payment))

    assert result is payment
    assert updates == [(465, "link-id", "canceled")]
    assert session.commits == 1
