"""
订阅支付回调单元测试

覆盖场景：
- 正确签名的回调处理（当前实现无签名验证，验证参数合法性）
- 缺失参数的回调被拒绝（422）
- 重复回调的幂等性（mark_paid 对已支付账单返回 False）
- 各种支付状态（成功处理流程）
- 不同套餐和计费周期

注：当前支付回调实现较简单（无签名验证，使用查询参数），
测试聚焦于回调处理的核心逻辑和边界情况。
"""

from __future__ import annotations

import os
import sys
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch

# ── 环境变量 & sys.path（必须在导入 app.* 之前设置） ──────────────────────────
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

from app.api.routes.subscription import _get_plan_name, _get_plan_price, router

# ---------------------------------------------------------------------------
# 1. Mock Billing Repository
# ---------------------------------------------------------------------------


@dataclass
class MockBillingRecord:
    id: str = ""
    user_id: str = ""
    plan_name: str = ""
    amount: float = 0.0
    billing_cycle: str = ""
    status: str = "pending"
    payment_method: str = ""
    payment_id: str = ""
    paid_at: datetime | None = None
    created_at: datetime | None = None

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)
        if self.created_at is None:
            self.created_at = datetime.now(timezone.utc)


class MockBillingRepository:
    """模拟的 Billing Repository，用于单元测试。"""

    def __init__(self):
        self.records: dict[str, MockBillingRecord] = {}
        self.created_count = 0
        self.mark_paid_count = 0
        self.update_subscription_count = 0
        self.updated_subscriptions: dict[str, dict] = {}

    def create(self, record: dict) -> MockBillingRecord:
        model = MockBillingRecord(**record)
        self.records[model.id] = model
        self.created_count += 1
        return model

    def find_by_user(self, user_id: str, limit: int = 50) -> list[MockBillingRecord]:
        items = [r for r in self.records.values() if r.user_id == user_id]
        items.sort(key=lambda r: r.created_at or datetime.min, reverse=True)
        return items[:limit]

    def find_by_id(self, record_id: str) -> MockBillingRecord | None:
        return self.records.get(record_id)

    def mark_paid(self, record_id: str, payment_method: str, payment_id: str) -> bool:
        self.mark_paid_count += 1
        model = self.records.get(record_id)
        if model is None or model.status == "paid":
            return False
        model.status = "paid"
        model.payment_method = payment_method
        model.payment_id = payment_id
        model.paid_at = datetime.now(timezone.utc)
        return True

    def update_subscription_on_payment(self, user_id: str, plan: str, expires_at: datetime) -> None:
        self.update_subscription_count += 1
        self.updated_subscriptions[user_id] = {
            "plan": plan,
            "expires_at": expires_at,
            "status": "active",
        }


# ---------------------------------------------------------------------------
# 2. Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_repo():
    return MockBillingRepository()


def _make_client(mock_billing_repo: MockBillingRepository) -> TestClient:
    """创建带有 mock billing repository 的 TestClient。"""
    test_app = FastAPI()
    test_app.include_router(router, prefix="/subscription")

    # Mock SessionLocal 和 BillingRepository
    mock_session = MagicMock()
    with patch(
        "packages.adapters.sqlalchemy_impl.session.SessionLocal",
        return_value=mock_session,
    ):
        with patch(
            "packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository",
            return_value=mock_billing_repo,
        ):
            yield TestClient(test_app)


# ---------------------------------------------------------------------------
# 3. 支付成功回调测试
# ---------------------------------------------------------------------------


class TestPaymentCallbackSuccess:
    """支付成功回调测试。"""

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_monthly_pro_payment_success(self, MockSession, MockRepo):
        """Pro 套餐月付支付成功。"""
        mock_repo = MockBillingRepository()
        MockRepo.return_value = mock_repo
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={
                "user_id": "user-001",
                "plan": "pro",
                "billing_cycle": "monthly",
                "amount": 299.0,
                "payment_method": "alipay",
                "payment_id": "pay_20240101_001",
            },
        )

        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is True
        assert "支付成功" in data["message"]
        assert "record_id" in data

        # 验证账单创建
        assert mock_repo.created_count == 1
        # 验证标记支付
        assert mock_repo.mark_paid_count == 1
        # 验证订阅更新
        assert mock_repo.update_subscription_count == 1
        assert "user-001" in mock_repo.updated_subscriptions
        assert mock_repo.updated_subscriptions["user-001"]["plan"] == "pro"

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_yearly_standard_payment_success(self, MockSession, MockRepo):
        """标准版年付支付成功。"""
        mock_repo = MockBillingRepository()
        MockRepo.return_value = mock_repo
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={
                "user_id": "user-002",
                "plan": "standard",
                "billing_cycle": "yearly",
                "amount": 999.0,
                "payment_method": "wechat",
                "payment_id": "wx_20240101_002",
            },
        )

        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is True
        assert mock_repo.updated_subscriptions["user-002"]["plan"] == "standard"
        # 年付到期时间应为约 365 天后
        expires_at = mock_repo.updated_subscriptions["user-002"]["expires_at"]
        expected = datetime.now(timezone.utc) + timedelta(days=365)
        assert abs((expires_at - expected).days) <= 1

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_enterprise_payment_success(self, MockSession, MockRepo):
        """企业版支付成功。"""
        mock_repo = MockBillingRepository()
        MockRepo.return_value = mock_repo
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={
                "user_id": "user-003",
                "plan": "enterprise",
                "billing_cycle": "monthly",
                "amount": 999.0,
                "payment_method": "bank_transfer",
                "payment_id": "ent_20240101_003",
            },
        )

        assert resp.status_code == 200
        assert resp.json()["success"] is True
        assert mock_repo.updated_subscriptions["user-003"]["plan"] == "enterprise"

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_default_payment_params(self, MockSession, MockRepo):
        """使用默认 payment_method 和空 payment_id。"""
        mock_repo = MockBillingRepository()
        MockRepo.return_value = mock_repo
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={
                "user_id": "user-004",
                "plan": "standard",
                "billing_cycle": "monthly",
                "amount": 99.0,
            },
        )

        assert resp.status_code == 200
        assert resp.json()["success"] is True
        # 默认 payment_method 应为 alipay
        assert mock_repo.mark_paid_count == 1


# ---------------------------------------------------------------------------
# 4. 重复回调幂等性测试
# ---------------------------------------------------------------------------


class TestPaymentCallbackIdempotency:
    """支付回调幂等性测试。"""

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_duplicate_callback_creates_new_record(self, MockSession, MockRepo):
        """重复回调（当前实现每次创建新账单，无幂等保护）。"""
        mock_repo = MockBillingRepository()
        MockRepo.return_value = mock_repo
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        params = {
            "user_id": "user-idem-1",
            "plan": "pro",
            "billing_cycle": "monthly",
            "amount": 299.0,
            "payment_id": "pay_dup_001",
        }

        # 第一次回调
        resp1 = client.post("/subscription/payment-callback", params=params)
        assert resp1.status_code == 200

        # 第二次回调（当前实现会创建新账单，不做幂等）
        resp2 = client.post("/subscription/payment-callback", params=params)
        assert resp2.status_code == 200
        # 当前实现每次都会创建新账单
        assert mock_repo.created_count == 2

    def test_mark_paid_is_idempotent(self):
        """mark_paid 方法对已支付账单返回 False（幂等）。"""
        repo = MockBillingRepository()

        repo.create(
            dict(
                id="bill-001",
                user_id="u1",
                plan_name="Pro 专业版",
                amount=299.0,
                billing_cycle="monthly",
                status="pending",
            )
        )

        # 第一次标记为已支付
        result1 = repo.mark_paid("bill-001", "alipay", "pay-001")
        assert result1 is True
        assert repo.records["bill-001"].status == "paid"

        # 第二次标记（幂等，应返回 False）
        result2 = repo.mark_paid("bill-001", "alipay", "pay-001")
        assert result2 is False
        assert repo.records["bill-001"].status == "paid"


# ---------------------------------------------------------------------------
# 5. 参数校验测试
# ---------------------------------------------------------------------------


class TestPaymentCallbackValidation:
    """支付回调参数校验测试。"""

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_missing_user_id_returns_422(self, MockSession, MockRepo):
        """缺少 user_id 参数返回 422。"""
        MockRepo.return_value = MockBillingRepository()
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={"plan": "pro", "billing_cycle": "monthly", "amount": 299.0},
        )
        assert resp.status_code == 422

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_missing_plan_returns_422(self, MockSession, MockRepo):
        """缺少 plan 参数返回 422。"""
        MockRepo.return_value = MockBillingRepository()
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={"user_id": "u1", "billing_cycle": "monthly", "amount": 299.0},
        )
        assert resp.status_code == 422

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_missing_amount_returns_422(self, MockSession, MockRepo):
        """缺少 amount 参数返回 422。"""
        MockRepo.return_value = MockBillingRepository()
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={"user_id": "u1", "plan": "pro", "billing_cycle": "monthly"},
        )
        assert resp.status_code == 422

    @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository")
    @patch("packages.adapters.sqlalchemy_impl.session.SessionLocal")
    def test_negative_amount(self, MockSession, MockRepo):
        """负数金额（当前实现不校验，记录此行为）。"""
        mock_repo = MockBillingRepository()
        MockRepo.return_value = mock_repo
        MockSession.return_value = MagicMock()

        test_app = FastAPI()
        test_app.include_router(router, prefix="/subscription")
        client = TestClient(test_app)

        resp = client.post(
            "/subscription/payment-callback",
            params={
                "user_id": "u1",
                "plan": "pro",
                "billing_cycle": "monthly",
                "amount": -100.0,
            },
        )
        # 当前实现未校验金额正负
        assert resp.status_code in (200, 400, 500)


# ---------------------------------------------------------------------------
# 6. 辅助函数测试
# ---------------------------------------------------------------------------


class TestHelperFunctions:
    """订阅辅助函数测试。"""

    def test_get_plan_name_all_plans(self):
        """所有套餐名称映射正确。"""
        assert _get_plan_name("free") == "体验版"
        assert _get_plan_name("standard") == "标准版"
        assert _get_plan_name("pro") == "专业版"
        assert _get_plan_name("enterprise") == "企业版"

    def test_get_plan_name_unknown(self):
        """未知套餐返回「未知套餐」。"""
        assert _get_plan_name("unknown") == "未知套餐"
        assert _get_plan_name("") == "未知套餐"

    def test_get_plan_price_all_combinations(self):
        """所有套餐价格映射正确。"""
        assert _get_plan_price("free", "monthly") == 0
        assert _get_plan_price("free", "yearly") == 0
        assert _get_plan_price("standard", "monthly") == 99
        assert _get_plan_price("standard", "yearly") == 999
        assert _get_plan_price("pro", "monthly") == 299
        assert _get_plan_price("pro", "yearly") == 2999
        assert _get_plan_price("enterprise", "monthly") == 999
        assert _get_plan_price("enterprise", "yearly") == 9999

    def test_get_plan_price_unknown(self):
        """未知组合返回 0。"""
        assert _get_plan_price("unknown", "monthly") == 0
        assert _get_plan_price("pro", "weekly") == 0


# ---------------------------------------------------------------------------
# 7. Mock Billing Repository 单元测试
# ---------------------------------------------------------------------------


class TestMockBillingRepository:
    """Billing Repository 行为单元测试。"""

    def test_create_record(self):
        """创建账单记录。"""
        repo = MockBillingRepository()
        record = repo.create(
            dict(
                id="bill-001",
                user_id="user-001",
                plan_name="Pro 专业版",
                amount=299.0,
                billing_cycle="monthly",
                status="pending",
            )
        )
        assert record.id == "bill-001"
        assert record.status == "pending"
        assert repo.created_count == 1

    def test_find_by_id(self):
        """按 ID 查询账单。"""
        repo = MockBillingRepository()
        repo.create(
            dict(
                id="bill-001",
                user_id="user-1",
                plan_name="Pro",
                amount=299,
                billing_cycle="monthly",
                status="pending",
            )
        )

        found = repo.find_by_id("bill-001")
        assert found is not None
        assert found.id == "bill-001"

        not_found = repo.find_by_id("nonexistent")
        assert not_found is None

    def test_find_by_user(self):
        """按用户查询账单。"""
        repo = MockBillingRepository()
        repo.create(dict(id="b1", user_id="u1", plan_name="Pro", amount=299, billing_cycle="monthly", status="pending"))
        repo.create(
            dict(id="b2", user_id="u1", plan_name="Standard", amount=99, billing_cycle="monthly", status="pending")
        )
        repo.create(dict(id="b3", user_id="u2", plan_name="Pro", amount=299, billing_cycle="monthly", status="pending"))

        user1_records = repo.find_by_user("u1")
        assert len(user1_records) == 2

        user2_records = repo.find_by_user("u2")
        assert len(user2_records) == 1

    def test_mark_paid_transitions_status(self):
        """mark_paid 正确转换状态。"""
        repo = MockBillingRepository()
        repo.create(
            dict(
                id="bill-001",
                user_id="u1",
                plan_name="Pro",
                amount=299,
                billing_cycle="monthly",
                status="pending",
            )
        )

        result = repo.mark_paid("bill-001", "alipay", "pay-001")
        assert result is True

        record = repo.find_by_id("bill-001")
        assert record.status == "paid"
        assert record.payment_method == "alipay"
        assert record.payment_id == "pay-001"
        assert record.paid_at is not None

    def test_mark_paid_idempotent(self):
        """mark_paid 对已支付账单幂等。"""
        repo = MockBillingRepository()
        repo.create(
            dict(
                id="bill-001",
                user_id="u1",
                plan_name="Pro",
                amount=299,
                billing_cycle="monthly",
                status="pending",
            )
        )

        repo.mark_paid("bill-001", "alipay", "pay-001")
        paid_at_first = repo.find_by_id("bill-001").paid_at

        result = repo.mark_paid("bill-001", "alipay", "pay-001")
        assert result is False
        # paid_at 不应更新
        assert repo.find_by_id("bill-001").paid_at == paid_at_first

    def test_mark_paid_nonexistent_returns_false(self):
        """标记不存在的账单返回 False。"""
        repo = MockBillingRepository()
        result = repo.mark_paid("nonexistent", "alipay", "pay-001")
        assert result is False

    def test_update_subscription_on_payment(self):
        """支付成功后更新订阅。"""
        repo = MockBillingRepository()
        expires = datetime.now(timezone.utc) + timedelta(days=30)

        repo.update_subscription_on_payment("user-001", "pro", expires)

        assert repo.update_subscription_count == 1
        assert "user-001" in repo.updated_subscriptions
        sub = repo.updated_subscriptions["user-001"]
        assert sub["plan"] == "pro"
        assert sub["status"] == "active"
        assert sub["expires_at"] == expires


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
