"""Mail 模块单测 + endpoint 集成测试。"""

from __future__ import annotations

import pytest
from fastapi.testclient import TestClient

from xiangqin import auth, mail


# ============ helpers ============


def _register_user(phone: str = "13800000001") -> str:
    r = auth.register(phone)
    v = auth.verify(r.request_id, "123456")
    return v.user_id


def _two_users() -> tuple[str, str]:
    return _register_user("13800001111"), _register_user("13800002222")


# ============ 基础发信 ============


def test_send_basic_uses_free_envelope():
    a, b = _two_users()
    msg = mail.send(a, b, "hello")
    assert msg.delivery_status == "delivered"
    assert msg.envelope_source == "free_daily"
    state = mail.get_envelope_state(a)
    assert state.free_remaining_today == 2


def test_send_cannot_self():
    a = _register_user()
    with pytest.raises(mail.MailError) as e:
        mail.send(a, a, "hi")
    assert e.value.code == "self_send"


def test_send_to_nonexistent_user_fails():
    a = _register_user()
    with pytest.raises(mail.MailError) as e:
        mail.send(a, "01JZZZZZZZZZZZZZZZZZZZZZZZ", "hello")
    assert e.value.code == "recipient_not_found"


def test_send_empty_body_rejected():
    a, b = _two_users()
    with pytest.raises(mail.MailError) as e:
        mail.send(a, b, "   ")
    assert e.value.code == "invalid_body"


def test_send_body_too_long():
    a, b = _two_users()
    with pytest.raises(mail.MailError):
        mail.send(a, b, "x" * (mail.BODY_MAX_LEN + 1))


# ============ 信封配额 ============


def test_exhaust_free_then_need_paid():
    a, b = _two_users()
    for _ in range(mail.FREE_ENVELOPES_PER_DAY):
        mail.send(a, b, "free")
    with pytest.raises(mail.MailError) as e:
        mail.send(a, b, "no envelope")
    assert e.value.code == "insufficient_envelopes"


def test_buy_envelopes_enables_paid_send():
    a, b = _two_users()
    # 耗尽免费
    for _ in range(mail.FREE_ENVELOPES_PER_DAY):
        mail.send(a, b, "free")
    # 买 2 个付费
    order = mail.buy_envelopes(a, count=2, mock=True)
    assert order["status"] == "paid"
    # 再发 2 条
    msg1 = mail.send(a, b, "paid 1")
    msg2 = mail.send(a, b, "paid 2")
    assert msg1.envelope_source == "paid"
    assert msg2.envelope_source == "paid"
    state = mail.get_envelope_state(a)
    assert state.paid_remaining == 0
    assert state.total_purchased == 2
    # 再发就挂
    with pytest.raises(mail.MailError):
        mail.send(a, b, "nope")


def test_buy_envelopes_real_creates_pending_order(monkeypatch):
    """mock=False 触发真付：调 alipay.create_precreate 拿 qr_code，订单 pending。"""
    from xiangqin import alipay as alipay_module

    monkeypatch.setattr(
        alipay_module,
        "create_precreate",
        lambda order_id, amount_cents, subject: "https://qr.alipay.com/fake-code",
    )
    a = _register_user()
    r = mail.buy_envelopes(a, count=3, mock=False)
    assert r["mock"] is False
    assert r["status"] == "pending"
    assert r["qr_code"] == "https://qr.alipay.com/fake-code"
    # 余量未变（等 webhook 回调才充值）
    s = mail.get_envelope_state(a)
    assert s.paid_remaining == 0


def test_buy_envelopes_real_propagates_alipay_error(monkeypatch):
    """alipay precreate 挂 → 订单标记 failed + 抛 payment_provider_error。"""
    from xiangqin import alipay as alipay_module

    def _boom(*a, **kw):
        raise alipay_module.AlipayError("fake network fail")

    monkeypatch.setattr(alipay_module, "create_precreate", _boom)
    a = _register_user()
    with pytest.raises(mail.MailError) as e:
        mail.buy_envelopes(a, count=1, mock=False)
    assert e.value.code == "payment_provider_error"


def test_alipay_webhook_paid_credits_balance(monkeypatch):
    """支付宝回调 paid 时，订单标 paid + 余量充值。"""
    from xiangqin import alipay as alipay_module

    monkeypatch.setattr(
        alipay_module,
        "create_precreate",
        lambda order_id, amount_cents, subject: "https://qr.alipay.com/fake",
    )
    a = _register_user()
    r = mail.buy_envelopes(a, count=5, mock=False)
    order_id = r["id"]

    # mock webhook 验签返 (order_id, 'paid')
    monkeypatch.setattr(
        alipay_module,
        "verify_webhook",
        lambda body: (order_id, "paid"),
    )
    result = mail.handle_alipay_webhook(b"fake-form-body")
    assert result["order_id"] == order_id
    assert result["status"] == "paid"
    assert result["action"] == "credited"

    s = mail.get_envelope_state(a)
    assert s.paid_remaining == 5


def test_alipay_webhook_idempotent(monkeypatch):
    """同一订单回调多次只充值一次。"""
    from xiangqin import alipay as alipay_module

    monkeypatch.setattr(alipay_module, "create_precreate", lambda *a, **kw: "qr")
    a = _register_user()
    r = mail.buy_envelopes(a, count=2, mock=False)
    oid = r["id"]
    monkeypatch.setattr(alipay_module, "verify_webhook", lambda body: (oid, "paid"))

    r1 = mail.handle_alipay_webhook(b"body-1")
    r2 = mail.handle_alipay_webhook(b"body-2")
    assert r1["action"] == "credited"
    assert r2["action"] == "noop"
    # 余量只 +2 一次
    s = mail.get_envelope_state(a)
    assert s.paid_remaining == 2


def test_get_order_returns_status(monkeypatch):
    """xq envelope order 查订单状态给 CLI 轮询用。"""
    from xiangqin import alipay as alipay_module

    monkeypatch.setattr(alipay_module, "create_precreate", lambda *a, **kw: "qr")
    a = _register_user()
    r = mail.buy_envelopes(a, count=1, mock=False)
    o = mail.get_order(a, r["id"])
    assert o is not None
    assert o["status"] == "pending"
    assert o["provider"] == "alipay"
    assert o["paid"] is False


def test_get_order_forbidden_for_others():
    a = _register_user(phone="13800001111")
    b = _register_user(phone="13800002222")
    r = mail.buy_envelopes(a, count=1, mock=True)
    with pytest.raises(mail.MailError) as ei:
        mail.get_order(b, r["id"])
    assert ei.value.code == "forbidden"


def test_buy_envelopes_count_bounds():
    a = _register_user()
    with pytest.raises(mail.MailError):
        mail.buy_envelopes(a, count=0, mock=True)
    with pytest.raises(mail.MailError):
        mail.buy_envelopes(a, count=99999, mock=True)


# ============ 拉黑 ============


def test_block_intercepts_subsequent_send():
    a, b = _two_users()
    mail.block(b, a)
    with pytest.raises(mail.MailError) as e:
        mail.send(a, b, "should fail")
    assert e.value.code == "blocked_by_recipient"


def test_block_does_not_consume_envelope():
    a, b = _two_users()
    mail.block(b, a)
    with pytest.raises(mail.MailError):
        mail.send(a, b, "fail")
    state = mail.get_envelope_state(a)
    assert state.free_remaining_today == mail.FREE_ENVELOPES_PER_DAY


def test_unblock_restores_send():
    a, b = _two_users()
    mail.block(b, a)
    mail.unblock(b, a)
    msg = mail.send(a, b, "works now")
    assert msg.delivery_status == "delivered"


def test_block_self_rejected():
    a = _register_user()
    with pytest.raises(mail.MailError):
        mail.block(a, a)


def test_blocklist_returns_items():
    a, b = _two_users()
    mail.block(a, b)
    items = mail.blocklist(a)
    assert len(items) == 1
    assert items[0]["blocked_id"] == b


# ============ 举报 ============


def test_report_records_log():
    a, b = _two_users()
    msg = mail.send(a, b, "hi")
    report = mail.report(b, msg.id, reason="骚扰")
    assert report["reported_user_id"] == a


def test_report_requires_recipient_role():
    a, b = _two_users()
    c = _register_user("13800003333")
    msg = mail.send(a, b, "hi")
    with pytest.raises(mail.MailError) as e:
        mail.report(c, msg.id, reason="nope")
    assert e.value.code == "forbidden"


# ============ 收件箱 ============


def test_inbox_lists_only_delivered_not_deleted():
    a, b = _two_users()
    m1 = mail.send(a, b, "one")
    m2 = mail.send(a, b, "two")
    inbox = mail.list_inbox(b)
    ids = {m.id for m in inbox}
    assert m1.id in ids and m2.id in ids


def test_read_message_marks_read():
    a, b = _two_users()
    msg = mail.send(a, b, "hi")
    assert msg.read_at is None
    read = mail.read_message(b, msg.id)
    assert read.read_at is not None


def test_read_forbidden_for_sender():
    a, b = _two_users()
    msg = mail.send(a, b, "hi")
    with pytest.raises(mail.MailError) as e:
        mail.read_message(a, msg.id)
    assert e.value.code == "forbidden"


def test_delete_by_recipient_hides_from_inbox():
    a, b = _two_users()
    msg = mail.send(a, b, "bye")
    mail.delete_message_by_recipient(b, msg.id)
    assert msg.id not in {m.id for m in mail.list_inbox(b)}


def test_reply_creates_new_message_and_links():
    a, b = _two_users()
    original = mail.send(a, b, "hi")
    replied = mail.reply(b, original.id, "hey back")
    assert replied.from_user_id == b
    assert replied.to_user_id == a
    # 原信已标 replied
    refreshed = mail.get_message(a, original.id)
    assert refreshed.replied_at is not None
    assert refreshed.reply_to_message_id == replied.id


def test_list_sent():
    a, b = _two_users()
    mail.send(a, b, "one")
    mail.send(a, b, "two")
    sent = mail.list_sent(a)
    assert len(sent) == 2


# ============ 通知去重 ============


def test_notify_not_enabled_without_gateway():
    from xiangqin import profile as profile_svc

    a, b = _two_users()
    msg = mail.send(a, b, "hi")
    # B 没配 gateway，notify_status 应该是 not_enabled
    refreshed = mail.get_message(a, msg.id)
    assert refreshed.notify_status == "not_enabled"

    # B 配了 URL 但没开 notify_on_new_mail
    profile_svc.update(
        b,
        {
            "agent_gateway_url": "http://127.0.0.1:1/hooks",
            "agent_hooks_token": "x",
            "notify_on_new_mail": False,
        },
    )
    msg2 = mail.send(a, b, "hi again")
    assert mail.get_message(a, msg2.id).notify_status == "not_enabled"


def test_notify_push_failed_when_gateway_down(monkeypatch):
    from xiangqin import profile as profile_svc

    a, b = _two_users()
    profile_svc.update(
        b,
        {
            "agent_gateway_url": "http://127.0.0.1:1/hooks",  # 故意不存在的端口
            "agent_hooks_token": "test",
            "notify_on_new_mail": True,
        },
    )
    msg = mail.send(a, b, "hi")
    refreshed = mail.get_message(a, msg.id)
    assert refreshed.notify_status == "push_failed"
    assert refreshed.notify_error is not None


def test_notify_dedup_suppresses_within_12h(monkeypatch):
    """模拟 push 成功后，12h 内有 unread 再来不推。"""
    from xiangqin import profile as profile_svc

    a, b = _two_users()
    profile_svc.update(
        b,
        {
            "agent_gateway_url": "http://test/hooks",
            "agent_hooks_token": "t",
            "notify_on_new_mail": True,
        },
    )

    # mock httpx.post 总返 200
    def fake_post(*args, **kwargs):
        class R:
            status_code = 200
            text = "ok"

        return R()

    monkeypatch.setattr(mail.httpx, "post", fake_post)

    msg1 = mail.send(a, b, "first")
    assert mail.get_message(a, msg1.id).notify_status == "pushed"

    # 第二封：有 unread + < 12h 应抑制
    msg2 = mail.send(a, b, "second")
    assert mail.get_message(a, msg2.id).notify_status == "suppressed_dup"


def test_notify_resumes_after_read(monkeypatch):
    """B 查过 inbox 后，新信应重新触发通知（当前 unread=0）。"""
    from xiangqin import profile as profile_svc

    a, b = _two_users()
    profile_svc.update(
        b,
        {
            "agent_gateway_url": "http://test/hooks",
            "agent_hooks_token": "t",
            "notify_on_new_mail": True,
        },
    )

    def fake_post(*args, **kwargs):
        class R:
            status_code = 200

        return R()

    monkeypatch.setattr(mail.httpx, "post", fake_post)

    msg1 = mail.send(a, b, "first")
    assert mail.get_message(a, msg1.id).notify_status == "pushed"

    # B 读了并标已读
    mail.read_message(b, msg1.id)

    # 第二封：现在 unread=0（第一封已读），应该再推
    msg2 = mail.send(a, b, "second")
    assert mail.get_message(a, msg2.id).notify_status == "pushed"


# ============ Endpoints (集成) ============


@pytest.fixture
def client():
    from xiangqin.app import app

    return TestClient(app)


def _login(client, phone: str) -> str:
    r = client.post("/auth/register", json={"phone": phone})
    req_id = r.json()["request_id"]
    v = client.post("/auth/verify", json={"request_id": req_id, "code": "123456"})
    return v.json()["session_token"]


def _login_user_id(client, phone: str) -> tuple[str, str]:
    r = client.post("/auth/register", json={"phone": phone})
    req_id = r.json()["request_id"]
    v = client.post("/auth/verify", json={"request_id": req_id, "code": "123456"})
    d = v.json()
    return d["session_token"], d["user_id"]


def test_dm_send_and_inbox_endpoint(client):
    tok_a, uid_a = _login_user_id(client, "13800004001")
    tok_b, uid_b = _login_user_id(client, "13800004002")

    r = client.post(
        "/dm/send",
        headers={"Authorization": f"Bearer {tok_a}"},
        json={"to_user_id": uid_b, "body": "你好"},
    )
    assert r.status_code == 200
    msg_id = r.json()["id"]

    inbox = client.get("/inbox", headers={"Authorization": f"Bearer {tok_b}"}).json()
    assert any(m["id"] == msg_id for m in inbox)


def test_dm_send_self_returns_400(client):
    tok, uid = _login_user_id(client, "13800004003")
    r = client.post(
        "/dm/send",
        headers={"Authorization": f"Bearer {tok}"},
        json={"to_user_id": uid, "body": "self"},
    )
    assert r.status_code == 400


def test_envelope_endpoints(client):
    tok_a = _login(client, "13800004004")
    r = client.get("/envelope", headers={"Authorization": f"Bearer {tok_a}"})
    assert r.status_code == 200
    s = r.json()
    assert s["free_remaining_today"] == mail.FREE_ENVELOPES_PER_DAY
    assert s["paid_remaining"] == 0

    buy = client.post(
        "/envelope/buy",
        headers={"Authorization": f"Bearer {tok_a}"},
        json={"count": 5, "mock": True},
    )
    assert buy.status_code == 200
    s2 = client.get("/envelope", headers={"Authorization": f"Bearer {tok_a}"}).json()
    assert s2["paid_remaining"] == 5


def test_inbox_reply_endpoint(client):
    tok_a, uid_a = _login_user_id(client, "13800004005")
    tok_b, uid_b = _login_user_id(client, "13800004006")
    sent = client.post(
        "/dm/send",
        headers={"Authorization": f"Bearer {tok_a}"},
        json={"to_user_id": uid_b, "body": "hi"},
    ).json()
    reply = client.post(
        f"/inbox/{sent['id']}/reply",
        headers={"Authorization": f"Bearer {tok_b}"},
        json={"body": "hey"},
    )
    assert reply.status_code == 200
    assert reply.json()["from_user_id"] == uid_b


def test_inbox_report_endpoint(client):
    tok_a, uid_a = _login_user_id(client, "13800004007")
    tok_b, uid_b = _login_user_id(client, "13800004008")
    sent = client.post(
        "/dm/send",
        headers={"Authorization": f"Bearer {tok_a}"},
        json={"to_user_id": uid_b, "body": "hi"},
    ).json()
    r = client.post(
        f"/inbox/{sent['id']}/report",
        headers={"Authorization": f"Bearer {tok_b}"},
        json={"reason": "骚扰"},
    )
    assert r.status_code == 200
    assert r.json()["reported_user_id"] == uid_a


def test_block_endpoint_then_blocked_send_fails(client):
    tok_a, uid_a = _login_user_id(client, "13800004009")
    tok_b, uid_b = _login_user_id(client, "13800004010")
    client.post(
        "/block",
        headers={"Authorization": f"Bearer {tok_b}"},
        json={"user_id": uid_a},
    )
    r = client.post(
        "/dm/send",
        headers={"Authorization": f"Bearer {tok_a}"},
        json={"to_user_id": uid_b, "body": "nope"},
    )
    assert r.status_code == 400


def test_profile_update_new_fields(client):
    tok = _login(client, "13800004011")
    r = client.put(
        "/profile/me",
        headers={"Authorization": f"Bearer {tok}"},
        json={
            "gender": "m",
            "age": 30,
            "city": "hangzhou",
            "tags": [],
            "bio": "",
            "height": 178,
            "education": "master",
            "agent_gateway_url": "https://test.example.com",
            "agent_hooks_token": "secret-token-value",
            "notify_on_new_mail": True,
        },
    )
    assert r.status_code == 200
    p = r.json()
    assert p["height"] == 178
    assert p["education"] == "master"
    assert p["agent_gateway_url"] == "https://test.example.com"
    # token 被 mask
    assert "***" in p["agent_hooks_token"]
    assert "secret-token-value" not in p["agent_hooks_token"]
    assert p["notify_on_new_mail"] is True


def test_profile_education_enum_enforced(client):
    tok = _login(client, "13800004012")
    r = client.put(
        "/profile/me",
        headers={"Authorization": f"Bearer {tok}"},
        json={"education": "high_school_graduate"},  # 不在枚举
    )
    assert r.status_code == 400


# ---- FEAT-018: push retry + admin retry endpoint ----


def test_push_retry_backoff_3_times(monkeypatch):
    """_push_notification 失败时 retry 3 次（含首次，2 次 retry），用 exp backoff。"""
    from xiangqin import profile as profile_svc

    a, b = _two_users()
    profile_svc.update(
        b,
        {
            "agent_gateway_url": "http://test/hooks",
            "agent_hooks_token": "t",
            "notify_on_new_mail": True,
        },
    )

    call_count = {"n": 0}
    sleep_args: list[float] = []

    def fake_post(*args, **kwargs):
        call_count["n"] += 1

        class R:
            status_code = 500
            text = "server error"

        return R()

    def fake_sleep(s):
        sleep_args.append(s)

    monkeypatch.setattr(mail.httpx, "post", fake_post)
    monkeypatch.setattr(mail.time, "sleep", fake_sleep)

    msg = mail.send(a, b, "should retry")
    assert call_count["n"] == 3  # 1 首次 + 2 retry
    assert sleep_args == [2, 5]  # backoff 顺序
    refreshed = mail.get_message(a, msg.id)
    assert refreshed.notify_status == "push_failed"
    assert "after 3 attempts" in (refreshed.notify_error or "")


def test_retry_failed_notifications_admin(monkeypatch):
    """admin retry_failed_notifications 扫 push_failed 重发；成功 / 失败 / 去重都正确计数。"""
    from xiangqin import profile as profile_svc

    a, b = _two_users()
    profile_svc.update(
        b,
        {
            "agent_gateway_url": "http://test/hooks",
            "agent_hooks_token": "t",
            "notify_on_new_mail": True,
        },
    )

    # 1. 第一轮：先让 push 失败两次（monkeypatch httpx 返 500）
    def failing_post(*args, **kwargs):
        class R:
            status_code = 500
            text = "x"

        return R()

    monkeypatch.setattr(mail.httpx, "post", failing_post)
    monkeypatch.setattr(mail.time, "sleep", lambda s: None)

    msg1 = mail.send(a, b, "first")
    msg2 = mail.send(a, b, "second")
    assert mail.get_message(a, msg1.id).notify_status == "push_failed"
    assert mail.get_message(a, msg2.id).notify_status == "push_failed"

    # 2. 换 httpx 成功，admin retry → 应该 2 条都 pushed
    def success_post(*args, **kwargs):
        class R:
            status_code = 200
            text = "ok"

        return R()

    monkeypatch.setattr(mail.httpx, "post", success_post)

    result = mail.retry_failed_notifications(limit=50, since_hours=24)
    # 注意：msg2 会被 12h 去重（msg1 刚成功 → unread ≥ 2 且 < 12h）
    assert result["scanned"] == 2
    # msg1 应 pushed，msg2 suppressed_dup
    statuses = [d["status"] for d in result["details"]]
    assert "pushed" in statuses
    # suppressed 因为 unread 逻辑（可能 1 或 0 取决于顺序）


# ============ FEAT-019 rate limit ============


def _topup_envelopes(user_id: str, count: int = 100) -> None:
    """测试助手：直接塞付费信封，绕过下单流程。"""
    from xiangqin.db import get_conn

    with get_conn() as conn:
        mail._credit_balance(conn, user_id, count)


def test_send_rate_limit_min_window(monkeypatch):
    """第 11 封信在 60s 内触发 mail_send_min 限流。"""
    from xiangqin import rate_limit

    a, b = _two_users()
    _topup_envelopes(a, 100)
    monkeypatch.setattr(rate_limit, "now_ts", lambda: 1_000_000)

    for i in range(10):
        mail.send(a, b, f"msg {i}")

    with pytest.raises(mail.MailError) as ei:
        mail.send(a, b, "over limit")
    assert ei.value.code == "rate_limited"


def test_send_rate_limit_per_user(monkeypatch):
    """限流按 user_id 隔离：A 打满 B 仍可发。"""
    from xiangqin import rate_limit

    a, b = _two_users()
    c = _register_user("13800009999")
    _topup_envelopes(a, 100)
    monkeypatch.setattr(rate_limit, "now_ts", lambda: 1_000_000)

    for i in range(10):
        mail.send(a, b, f"msg {i}")

    # c 不受 a 影响
    mail.send(c, b, "hello from c")


def test_send_rate_limit_endpoint_returns_429(client):
    """HTTP 层 /dm/send 超限映射到 429。"""
    tok_a, uid_a = _login_user_id(client, "13801110000")
    _, uid_b = _login_user_id(client, "13801110001")
    _topup_envelopes(uid_a, 100)
    hdr = {"Authorization": f"Bearer {tok_a}"}

    for i in range(10):
        r = client.post(
            "/dm/send",
            headers=hdr,
            json={"to_user_id": uid_b, "body": f"msg {i}"},
        )
        assert r.status_code == 200

    r = client.post(
        "/dm/send",
        headers=hdr,
        json={"to_user_id": uid_b, "body": "over"},
    )
    assert r.status_code == 429
    assert r.json()["detail"]["code"] == "rate_limited"
