"""阿里云短信 provider(Mode A)单元测试。

SDK 交互隔离在 aliyun._call_send / aliyun._call_check 两个薄封装,本文件全程 monkeypatch
它们(返回归一化结果 dict 或抛 SmsError)→ 不触真 SDK、不发网络。测的是 provider 的可映射逻辑:
错误码→HTTP 码、PASS/UNKNOWN 解释、本地失败计数(与极光同语义)、mock 短路。
"""
from __future__ import annotations

import pytest

from app.core.config import settings
from app.integrations.sms import aliyun
from app.integrations.sms.base import SmsError

PHONE = "13800138000"


def _configure(monkeypatch, *, mock: bool = False) -> None:
    """配齐阿里云凭证 + 设 SMS_MOCK;清本地失败计数隔离用例。"""
    monkeypatch.setattr(settings, "SMS_MOCK", mock)
    monkeypatch.setattr(settings, "ALIYUN_SMS_ACCESS_KEY_ID", "ak")
    monkeypatch.setattr(settings, "ALIYUN_SMS_ACCESS_KEY_SECRET", "sk")
    monkeypatch.setattr(settings, "ALIYUN_SMS_SIGN_NAME", "恒创联众")
    monkeypatch.setattr(settings, "ALIYUN_SMS_TEMPLATE_CODE", "SMS_100001")
    aliyun._verify_attempts.clear()


def _send_ok(phone):
    return {"success": True, "code": "OK", "message": "成功", "verify_code": "1234"}


def _check(result):
    def _f(phone, code):
        return {"success": True, "code": "OK", "message": "成功", "verify_result": result}
    return _f


# ============================ 发码 ============================

def test_send_success_returns_interval_and_resets_attempts(monkeypatch) -> None:
    _configure(monkeypatch)
    aliyun._verify_attempts[PHONE] = 3  # 旧失败计数
    monkeypatch.setattr(aliyun, "_call_send", _send_ok)

    assert aliyun.send_code(PHONE) == settings.ALIYUN_SMS_INTERVAL_SEC
    assert PHONE not in aliyun._verify_attempts  # 新码 = 新预算


@pytest.mark.parametrize(
    "code,expected",
    [
        ("MOBILE_NUMBER_ILLEGAL", 400),
        ("BUSINESS_LIMIT_CONTROL", 429),
        ("FREQUENCY_FAIL", 429),
        ("FUNCTION_NOT_OPENED", 503),
        ("INVALID_PARAMETERS", 503),
        ("SOME_UNEXPECTED_CODE", 503),
    ],
)
def test_send_maps_error_codes(monkeypatch, code, expected) -> None:
    _configure(monkeypatch)
    monkeypatch.setattr(
        aliyun, "_call_send",
        lambda phone: {"success": False, "code": code, "message": code, "verify_code": None},
    )
    with pytest.raises(SmsError) as ei:
        aliyun.send_code(PHONE)
    assert ei.value.status_code == expected


def test_send_not_configured_raises_503_without_calling_aliyun(monkeypatch) -> None:
    monkeypatch.setattr(settings, "SMS_MOCK", False)
    monkeypatch.setattr(settings, "ALIYUN_SMS_ACCESS_KEY_ID", "")  # 凭证缺

    def _boom(phone):
        raise AssertionError("未配置时不应调用阿里云")

    monkeypatch.setattr(aliyun, "_call_send", _boom)
    with pytest.raises(SmsError) as ei:
        aliyun.send_code(PHONE)
    assert ei.value.status_code == 503


def test_send_transport_error_propagates_503(monkeypatch) -> None:
    _configure(monkeypatch)

    def _boom(phone):
        raise SmsError("network down", status_code=503)

    monkeypatch.setattr(aliyun, "_call_send", _boom)
    with pytest.raises(SmsError) as ei:
        aliyun.send_code(PHONE)
    assert ei.value.status_code == 503


def test_send_mock_returns_interval_no_network(monkeypatch) -> None:
    _configure(monkeypatch, mock=True)

    def _boom(phone):
        raise AssertionError("mock 不应调用阿里云")

    monkeypatch.setattr(aliyun, "_call_send", _boom)
    assert aliyun.send_code(PHONE) == settings.ALIYUN_SMS_INTERVAL_SEC


# ============================ 校验 ============================

def test_verify_pass_true_and_clears_attempts(monkeypatch) -> None:
    _configure(monkeypatch)
    aliyun._verify_attempts[PHONE] = 2
    monkeypatch.setattr(aliyun, "_call_check", _check("PASS"))

    assert aliyun.verify_code(PHONE, "1234") is True
    assert PHONE not in aliyun._verify_attempts  # 验过即清


def test_verify_unknown_false_and_increments(monkeypatch) -> None:
    _configure(monkeypatch)
    monkeypatch.setattr(aliyun, "_call_check", _check("UNKNOWN"))

    assert aliyun.verify_code(PHONE, "0000") is False
    assert aliyun._verify_attempts[PHONE] == 1
    assert aliyun.verify_code(PHONE, "0000") is False
    assert aliyun._verify_attempts[PHONE] == 2


def test_verify_attempts_cap_short_circuits(monkeypatch) -> None:
    _configure(monkeypatch)
    aliyun._verify_attempts[PHONE] = settings.SMS_MAX_VERIFY_ATTEMPTS

    def _boom(phone, code):
        raise AssertionError("达失败上限后不应再调阿里云")

    monkeypatch.setattr(aliyun, "_call_check", _boom)
    assert aliyun.verify_code(PHONE, "1234") is False  # 本地作废


def test_verify_api_error_raises_503(monkeypatch) -> None:
    _configure(monkeypatch)
    monkeypatch.setattr(
        aliyun, "_call_check",
        lambda phone, code: {"success": False, "code": "SYSTEM_ERROR",
                             "message": "err", "verify_result": None},
    )
    with pytest.raises(SmsError) as ei:
        aliyun.verify_code(PHONE, "1234")
    assert ei.value.status_code == 503


def test_verify_transport_error_raises_503(monkeypatch) -> None:
    _configure(monkeypatch)

    def _boom(phone, code):
        raise SmsError("network down", status_code=503)

    monkeypatch.setattr(aliyun, "_call_check", _boom)
    with pytest.raises(SmsError) as ei:
        aliyun.verify_code(PHONE, "1234")
    assert ei.value.status_code == 503


def test_verify_mock_passes_any_ndigit(monkeypatch) -> None:
    _configure(monkeypatch, mock=True)

    def _boom(phone, code):
        raise AssertionError("mock 不应调用阿里云")

    monkeypatch.setattr(aliyun, "_call_check", _boom)
    assert aliyun.verify_code(PHONE, "123456") is True   # 6 位数字放行
    assert aliyun.verify_code(PHONE, "12345") is False   # 位数不对


# ============================ 端点:阿里云降级 → 503(auth.py 包 try/except)============================

def test_sms_login_aliyun_outage_returns_503(client, monkeypatch) -> None:
    """SMS_PROVIDER=aliyun 且校验时阿里云异常 → /sms/login 返 503(而非 400/500),便于区分排查。"""
    _configure(monkeypatch)  # 配齐凭证 + SMS_MOCK=False + 清计数
    monkeypatch.setattr(settings, "SMS_PROVIDER", "aliyun")

    def _boom(phone, code):
        raise SmsError("aliyun down", status_code=503)

    monkeypatch.setattr(aliyun, "_call_check", _boom)
    r = client.post("/api/v1/auth/sms/login", json={"phone": "13812345678", "code": "1234"})
    assert r.status_code == 503, r.text
