"""Integration tests for app.api.v1.subscriptions — plans, my-plan, tags, subscribe, upgrade, payment"""

import uuid

import pytest
import pytest_asyncio
from sqlalchemy import select

from app.models.user import Tenant, UserTenant
from app.models.literature import GlobalTag
from app.models.interaction import UserSubscription


@pytest_asyncio.fixture
async def auth_ctx(client):
    """Register a user and return (headers, user_id)."""
    email = f"t{uuid.uuid4().hex[:6]}@test.cn"
    resp = await client.post("/api/v1/auth/register", json={
        "email": email, "password": "Test1234", "display_name": "TestUser",
    })
    assert resp.status_code == 200, f"Register failed: {resp.text}"
    data = resp.json()
    headers = {"Authorization": f"Bearer {data['token']['access_token']}"}
    user_id = uuid.UUID(data["user"]["id"])
    return headers, user_id


@pytest_asyncio.fixture
async def seed_tags(db):
    """Create a few GlobalTag entries."""
    tags = []
    for name in ["肺癌", "乳腺癌", "免疫治疗"]:
        tag = GlobalTag(
            id=uuid.uuid4(), name_zh=name, name_en=name,
            path=f"diseases::{name}", tag_category="cancer", level=1,
        )
        db.add(tag)
        tags.append(tag)
    await db.commit()
    return tags


@pytest_asyncio.fixture
async def auth_and_tags(auth_ctx, db, seed_tags):
    """Returns (headers, user_id, tags) with tags and tenant ready."""
    return *auth_ctx, seed_tags


# ── Plans ──


@pytest.mark.asyncio
async def test_list_plans(client):
    """List plans returns 4 plans"""
    r = await client.get("/api/v1/subscriptions/plans")
    assert r.status_code == 200
    data = r.json()
    assert len(data["plans"]) == 3
    keys = {p["key"] for p in data["plans"]}
    assert keys == {"free", "pro", "team"}


@pytest.mark.asyncio
async def test_my_plan(client, auth_ctx):
    """My plan returns current plan info"""
    headers, _ = auth_ctx
    r = await client.get("/api/v1/subscriptions/my-plan", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert data["plan_key"] == "free"
    assert "features" in data
    assert "usage" in data
    assert data["usage"]["saved_limit"] is not None


# ── Tag Subscriptions ──


@pytest.mark.asyncio
async def test_list_tags_empty(client, auth_ctx):
    """List my tags returns empty list when none subscribed"""
    headers, _ = auth_ctx
    r = await client.get("/api/v1/subscriptions/my-tags", headers=headers)
    assert r.status_code == 200
    assert r.json() == []


@pytest.mark.asyncio
async def test_list_tags_with_data(client, auth_ctx, db, seed_tags):
    """List my tags returns active subscriptions"""
    headers, uid = auth_ctx
    # Get the tenant_id for this user
    r = await db.execute(
        select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid)
    )
    tenant_id = r.scalar()
    # Create a subscription
    db.add(UserSubscription(
        id=uuid.uuid4(), user_id=uid, tenant_id=tenant_id,
        tag_id=seed_tags[0].id, match_mode="standard", is_active=True,
    ))
    await db.commit()

    r = await client.get("/api/v1/subscriptions/my-tags", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert len(data) >= 1
    assert data[0]["tag_name"] == "肺癌"


@pytest.mark.asyncio
async def test_toggle_subscribe(client, auth_ctx, db, seed_tags):
    """Toggle subscribe creates a new subscription"""
    headers, uid = auth_ctx
    r = await client.post(
        "/api/v1/subscriptions/toggle",
        json={"tag_id": str(seed_tags[0].id), "subscribe": True},
        headers=headers,
    )
    assert r.status_code == 200
    assert r.json()["status"] == "subscribed"

    # Verify in DB
    r2 = await client.get("/api/v1/subscriptions/my-tags", headers=headers)
    assert any(t["tag_name"] == "肺癌" for t in r2.json())


@pytest.mark.asyncio
async def test_toggle_unsubscribe(client, auth_ctx, db, seed_tags):
    """Toggle unsubscribe deactivates a subscription"""
    headers, uid = auth_ctx
    r = await db.execute(
        select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid)
    )
    tenant_id = r.scalar()
    sub = UserSubscription(
        id=uuid.uuid4(), user_id=uid, tenant_id=tenant_id,
        tag_id=seed_tags[0].id, match_mode="standard", is_active=True,
    )
    db.add(sub)
    await db.commit()

    r = await client.post(
        "/api/v1/subscriptions/toggle",
        json={"tag_id": str(seed_tags[0].id), "subscribe": False},
        headers=headers,
    )
    assert r.status_code == 200
    assert r.json()["status"] == "unsubscribed"


@pytest.mark.asyncio
async def test_toggle_update(client, auth_ctx, db, seed_tags):
    """Toggle subscribe on existing sub updates match_mode."""
    headers, uid = auth_ctx
    r = await db.execute(select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid))
    tenant_id = r.scalar()
    db.add(UserSubscription(id=uuid.uuid4(), user_id=uid, tenant_id=tenant_id,
                            tag_id=seed_tags[0].id, match_mode="standard", is_active=True))
    await db.commit()

    r = await client.post("/api/v1/subscriptions/toggle",
                          json={"tag_id": str(seed_tags[0].id), "subscribe": True, "match_mode": "fuzzy"},
                          headers=headers)
    assert r.status_code == 200
    assert r.json()["status"] == "updated"


@pytest.mark.asyncio
async def test_toggle_no_change(client, auth_ctx, db, seed_tags):
    """Toggle unsubscribe on non-subscribed tag returns no_change."""
    headers, _ = auth_ctx
    r = await client.post("/api/v1/subscriptions/toggle",
                          json={"tag_id": str(seed_tags[0].id), "subscribe": False},
                          headers=headers)
    assert r.status_code == 200
    assert r.json()["status"] == "no_change"


@pytest.mark.asyncio
async def test_toggle_subscribe_no_tenant(client, auth_ctx, db, seed_tags):
    """Toggle without tenant context returns 400"""
    headers, _ = auth_ctx
    r = await client.post(
        "/api/v1/subscriptions/toggle",
        json={"tag_id": str(seed_tags[0].id), "subscribe": True},
        headers=headers,
    )
    # tenant_ctx should be set by auth — this should succeed
    assert r.status_code in (200, 400)


# ── Batch Subscribe ──


@pytest.mark.asyncio
async def test_batch_subscribe(client, auth_ctx, db, seed_tags):
    """Batch subscribe creates subscriptions and generates feeds"""
    headers, _ = auth_ctx
    tag_ids = [str(t.id) for t in seed_tags]
    r = await client.post(
        "/api/v1/subscriptions/batch-subscribe",
        json={"tag_ids": tag_ids, "match_mode": "standard"},
        headers=headers,
    )
    # If tenant_ctx is not set, it returns 400
    if r.status_code == 200:
        assert r.json()["status"] == "ok"
        assert r.json()["subscribed"] == len(seed_tags)
    else:
        assert r.status_code == 400


# ── Upgrade Preview ──


@pytest.mark.asyncio
async def test_preview_upgrade(client, auth_ctx):
    """Preview upgrade shows plan differences"""
    headers, _ = auth_ctx
    r = await client.post(
        "/api/v1/subscriptions/upgrade/preview?target_plan=pro",
        headers=headers,
    )
    assert r.status_code == 200
    data = r.json()
    assert data["current_plan"] == "Free"
    assert data["target_plan"] == "Pro"
    assert "changes" in data


@pytest.mark.asyncio
async def test_upgrade_pro_requires_payment(client, auth_ctx):
    """Upgrade to pro requires payment"""
    headers, _ = auth_ctx
    r = await client.post(
        "/api/v1/subscriptions/upgrade",
        json={"target_plan": "pro"},
        headers=headers,
    )
    assert r.status_code == 200
    assert r.json()["status"] == "payment_required"


@pytest.mark.asyncio
async def test_upgrade_invalid_plan(client, auth_ctx):
    """Upgrade with invalid plan returns 400"""
    headers, _ = auth_ctx
    r = await client.post(
        "/api/v1/subscriptions/upgrade",
        json={"target_plan": "nonexistent"},
        headers=headers,
    )
    assert r.status_code == 400


# ── Payment Status ──


@pytest.mark.asyncio
async def test_payment_status_not_found(client, auth_ctx):
    """Payment status for non-existent order returns 404"""
    headers, _ = auth_ctx
    r = await client.get(
        f"/api/v1/subscriptions/payment/{uuid.uuid4()}/status",
        headers=headers,
    )
    assert r.status_code == 404


# ── Payment (dev mode) ──


@pytest.mark.asyncio
async def test_batch_subscribe_reexisting(client, auth_ctx, db, seed_tags):
    """Batch subscribe again updates existing subscriptions."""
    headers, uid = auth_ctx
    r = await db.execute(select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid))
    tenant_id = r.scalar()
    db.add(UserSubscription(id=uuid.uuid4(), user_id=uid, tenant_id=tenant_id,
                            tag_id=seed_tags[0].id, match_mode="standard", is_active=True))
    await db.commit()

    r = await client.post("/api/v1/subscriptions/batch-subscribe",
                          json={"tag_ids": [str(seed_tags[0].id)], "match_mode": "fuzzy"},
                          headers=headers)
    if r.status_code == 200:
        assert r.json()["subscribed"] == 1
        assert r.json()["status"] == "ok"



@pytest.mark.asyncio
async def test_payment_status_success(client, auth_ctx, db):
    """Payment status returns order info after mock payment."""
    headers, uid = auth_ctx
    r = await db.execute(select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid))
    tenant_id = r.scalar()

    # Create order
    from app.models.operations import PaymentOrder
    order = PaymentOrder(id=uuid.uuid4(), tenant_id=tenant_id, user_id=uid,
                         plan="pro", amount=3500, status="paid",
                         out_trade_no=f"mock_{uuid.uuid4().hex[:12]}")
    db.add(order)
    await db.commit()

    r = await client.get(f"/api/v1/subscriptions/payment/{order.id}/status",
                         headers=headers)
    assert r.status_code == 200
    assert r.json()["status"] == "paid"
    assert r.json()["plan"] == "pro"


@pytest.mark.asyncio
async def test_payment_status_forbidden(client, auth_ctx, db):
    """Payment status for another user's order returns 403."""
    headers, uid = auth_ctx
    r = await db.execute(select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid))
    tenant_id = r.scalar()

    other_user_id = uuid.uuid4()
    from app.models.operations import PaymentOrder
    order = PaymentOrder(id=uuid.uuid4(), tenant_id=tenant_id, user_id=other_user_id,
                         plan="pro", amount=3500, status="pending",
                         out_trade_no=f"mock_{uuid.uuid4().hex[:12]}")
    db.add(order)
    await db.commit()

    r = await client.get(f"/api/v1/subscriptions/payment/{order.id}/status",
                         headers=headers)
    assert r.status_code == 403


@pytest.mark.asyncio
async def test_mock_payment_not_found(client, auth_ctx):
    """Mock payment on non-existent order returns 404."""
    headers, _ = auth_ctx
    r = await client.post(f"/api/v1/subscriptions/payment/{uuid.uuid4()}/mock-paid",
                          headers=headers)
    assert r.status_code == 404


@pytest.mark.asyncio
async def test_mock_payment_wrong_status(client, auth_ctx, db):
    """Mock payment on already-paid order returns 400."""
    headers, uid = auth_ctx
    r = await db.execute(select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid))
    tenant_id = r.scalar()

    from app.models.operations import PaymentOrder
    order = PaymentOrder(id=uuid.uuid4(), tenant_id=tenant_id, user_id=uid,
                         plan="pro", amount=3500, status="paid",
                         out_trade_no=f"mock_{uuid.uuid4().hex[:12]}")
    db.add(order)
    await db.commit()

    r = await client.post(f"/api/v1/subscriptions/payment/{order.id}/mock-paid",
                          headers=headers)
    assert r.status_code == 400


@pytest.mark.asyncio
async def test_create_wechat_order(client, auth_ctx, db):
    """Create WeChat pay order (may fail in dev mode without WeChat config)"""
    headers, uid = auth_ctx
    # Get tenant_id
    r = await db.execute(
        select(Tenant.id).join(UserTenant).where(UserTenant.user_id == uid)
    )
    tenant_id = r.scalar()

    # Try to create a pro plan order
    resp = await client.post(
        "/api/v1/subscriptions/wechat-pay/create?plan=pro",
        headers=headers,
    )
    # May succeed or fail depending on WeChat config
    if resp.status_code == 200:
        data = resp.json()
        assert "order_id" in data
        assert data["plan"] == "pro"

        # Mock payment
        order_id = data["order_id"]
        r = await client.post(
            f"/api/v1/subscriptions/payment/{order_id}/mock-paid",
            headers=headers,
        )
        if r.status_code == 200:
            assert r.json()["status"] == "paid"
    else:
        # 502 if create_unified_order fails (no WeChat config)
        assert resp.status_code in (400, 502)


# ── Preferences ──


@pytest.mark.asyncio
async def test_get_preferences_defaults(client, auth_ctx):
    """GET /preferences returns 200 with default preference keys."""
    headers, _ = auth_ctx
    r = await client.get("/api/v1/subscriptions/preferences", headers=headers)
    assert r.status_code == 200
    data = r.json()
    assert "journal_tiers" in data
    assert "therapy_types" in data
    assert "lit_types" in data
    assert "match_mode" in data
    # Defaults: empty lists and "standard"
    assert data["journal_tiers"] == []
    assert data["therapy_types"] == []
    assert data["lit_types"] == []
    assert data["match_mode"] == "standard"


@pytest.mark.asyncio
async def test_save_preferences(client, auth_ctx):
    """POST /preferences saves and returns status updated."""
    headers, _ = auth_ctx
    payload = {"journal_tiers": ["1", "2"], "therapy_types": ["targeted"]}
    r = await client.post(
        "/api/v1/subscriptions/preferences",
        json=payload,
        headers=headers,
    )
    assert r.status_code == 200
    data = r.json()
    assert data["status"] == "saved"
    assert data["preferences"]["journal_tiers"] == ["1", "2"]
    assert data["preferences"]["therapy_types"] == ["targeted"]

    # Verify persistence by re-fetching
    r2 = await client.get("/api/v1/subscriptions/preferences", headers=headers)
    assert r2.status_code == 200
    saved = r2.json()
    assert saved["journal_tiers"] == ["1", "2"]
    assert saved["therapy_types"] == ["targeted"]
