# =============================================================================
# 企微IT智能服务台 — 扫码登录 API 测试
# =============================================================================
# 测试覆盖:
#   1. create → 返回 ticket + qrcode_url
#   2. create 后立即 poll (waiting)
#   3. dev 模式 scan → 写 Redis scan:{ticket} 成功
#   4. scan 后 poll → scanned
#   5. dev 模式 confirm (无 otp) → 返回 token
#   6. confirm 后 poll → confirmed + token
#   7. 不存在的 ticket poll → expired
#   8. expired ticket confirm → 失败
#
# dev 模式强制走 mock(代码内 _dev_mode_enabled() 检查 DEV_MODE env),
# 测试通过 monkeypatch 强制开启,确保不调真实企微 API。
# =============================================================================

import pytest
import pytest_asyncio
from unittest.mock import patch

from tests.conftest import MockRedis


# --------------------------------------------------------------------------
# 工具: 让测试期间 dev 模式强制为 True
# --------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def force_dev_mode(monkeypatch):
    """强制 dev 模式为 True(让 _dev_mode_enabled() 返回 True)。

    通过同时 patch:
      1. os.getenv("DEV_MODE") → "true"
      2. settings.dev_mode → True
    避免真实企微 API 被调用。
    """
    monkeypatch.setenv("DEV_MODE", "true")
    from app.config import settings
    monkeypatch.setattr(settings, "dev_mode", True)
    yield


# --------------------------------------------------------------------------
# 工具: 创建已登录坐席 token,用于 confirm 端点鉴权
# --------------------------------------------------------------------------
async def _create_agent_token(mock_redis: MockRedis, user_id: str, name: str) -> str:
    """在 mock_redis 里手动写一个坐席 token,返回 token 字符串。

    与 TokenService.create_token 一致: 写 user:token:{token} + agent:token:{token}。
    """
    import json
    import secrets
    from datetime import datetime

    token = secrets.token_urlsafe(32)
    token_data = {
        "employee_id": user_id,
        "name": name,
        "department": "信息技术部",
        "avatar": "",
        "roles": ["agent"],
        "current_role": "agent",
        "login_source": "test",
        "created_at": datetime.now().isoformat(),
        "last_active": datetime.now().isoformat(),
    }
    # MockRedis 的 setex 内部用 str 存,get 返回 bytes
    await mock_redis.setex(
        f"user:token:{token}",
        8 * 60 * 60,
        json.dumps(token_data, ensure_ascii=False),
    )
    await mock_redis.setex(f"agent:token:{token}", 8 * 60 * 60, user_id)
    return token


# --------------------------------------------------------------------------
# 1. create: 返回 ticket + qrcode_url
# --------------------------------------------------------------------------
class TestQrcodeCreate:
    """测试创建扫码登录票据。"""

    @pytest.mark.asyncio
    async def test_create_returns_ticket_and_url(self, client, mock_redis):
        """验证 create 返回 ticket + 企微 OAuth2 URL。"""
        response = await client.post("/auth_qrcode/create")

        assert response.status_code == 200
        body = response.json()
        assert body["code"] == 0
        assert "data" in body

        data = body["data"]
        assert "ticket" in data
        assert len(data["ticket"]) >= 16
        assert "qrcode_url" in data
        # URL 必须含企微 OAuth 域名 + state={ticket}
        assert "open.weixin.qq.com/connect/oauth2/authorize" in data["qrcode_url"]
        assert f"state={data['ticket']}" in data["qrcode_url"]
        # 有效期 120s
        assert data["expires_in"] == 120
        assert "expires_at" in data

    @pytest.mark.asyncio
    async def test_create_writes_ticket_to_redis(self, client, mock_redis):
        """验证 create 后 Redis 写入了 qrcode:ticket:{ticket}。"""
        response = await client.post("/auth_qrcode/create")
        ticket = response.json()["data"]["ticket"]

        redis_key = f"qrcode:ticket:{ticket}"
        stored = await mock_redis.get(redis_key)
        assert stored is not None
        # stored 是 bytes(MockRedis.get 返回 bytes),解码后应含 created_at
        import json
        payload = json.loads(stored.decode("utf-8"))
        assert "created_at" in payload
        assert "expires_at" in payload


# --------------------------------------------------------------------------
# 2. create 后立即 poll → waiting
# --------------------------------------------------------------------------
class TestQrcodePoll:
    """测试轮询扫码状态。"""

    @pytest.mark.asyncio
    async def test_poll_after_create_returns_waiting(self, client, mock_redis):
        """create 后立即 poll,无扫码无确认,应为 waiting。"""
        # 1. create
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]

        # 2. poll
        poll_resp = await client.get(f"/auth_qrcode/poll/{ticket}")

        assert poll_resp.status_code == 200
        body = poll_resp.json()
        assert body["code"] == 0
        data = body["data"]
        assert data["status"] == "waiting"
        assert data["employee_id"] is None
        assert data["name"] is None
        assert data["token"] is None

    @pytest.mark.asyncio
    async def test_poll_nonexistent_ticket_returns_expired(self, client, mock_redis):
        """不存在的 ticket poll → expired。"""
        response = await client.get("/auth_qrcode/poll/nonexistent-ticket-xxx")

        assert response.status_code == 200
        body = response.json()
        assert body["code"] == 0
        assert body["data"]["status"] == "expired"
        assert body["data"]["token"] is None


# --------------------------------------------------------------------------
# 3+4. dev 模式 scan → scanned
# --------------------------------------------------------------------------
class TestQrcodeScan:
    """测试扫码回调(dev 模式强制 mock)。"""

    @pytest.mark.asyncio
    async def test_scan_writes_redis(self, client, mock_redis):
        """dev 模式 scan → 写 Redis scan:{ticket} 成功。"""
        # 1. create
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]

        # 2. scan(dev 模式 code 形如 "dev:dev-user-001")
        scan_resp = await client.post(
            "/auth_qrcode/scan",
            json={"ticket": ticket, "code": "dev:dev-user-001"},
        )

        assert scan_resp.status_code == 200
        body = scan_resp.json()
        assert body["code"] == 0
        assert body["data"]["success"] is True

        # 3. 验证 Redis 写入
        scan_key = f"qrcode:scan:{ticket}"
        stored = await mock_redis.get(scan_key)
        assert stored is not None
        import json
        payload = json.loads(stored.decode("utf-8"))
        assert payload["employee_id"] == "dev-user-001"
        assert "张三" in payload["name"]

    @pytest.mark.asyncio
    async def test_scan_then_poll_returns_scanned(self, client, mock_redis):
        """scan 后 poll → status=scanned,带 employee_id/name 但无 token。"""
        # create + scan
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]
        await client.post(
            "/auth_qrcode/scan",
            json={"ticket": ticket, "code": "dev:dev-agent-001"},
        )

        # poll
        poll_resp = await client.get(f"/auth_qrcode/poll/{ticket}")
        body = poll_resp.json()
        data = body["data"]

        assert data["status"] == "scanned"
        assert data["employee_id"] == "dev-agent-001"
        assert "李四" in data["name"]
        assert data["token"] is None

    @pytest.mark.asyncio
    async def test_scan_with_invalid_ticket_returns_error(self, client, mock_redis):
        """不存在的 ticket scan → 1003 错误。"""
        response = await client.post(
            "/auth_qrcode/scan",
            json={"ticket": "invalid-ticket-xxx", "code": "dev:dev-user-001"},
        )

        assert response.status_code == 200
        body = response.json()
        # 业务错误(票据过期),code 是错误码(非 0)
        assert body["code"] != 0
        assert body["code"] == 1003


# --------------------------------------------------------------------------
# 5+6. confirm: 无 otp → 返回 token,确认后 poll → confirmed+token
# --------------------------------------------------------------------------
class TestQrcodeConfirm:
    """测试已登录坐席确认授权。"""

    @pytest.mark.asyncio
    async def test_confirm_returns_token(self, client, mock_redis):
        """完整流程: create → scan → confirm → 返回 token。"""
        # 1. create
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]

        # 2. scan
        await client.post(
            "/auth_qrcode/scan",
            json={"ticket": ticket, "code": "dev:dev-user-001"},
        )

        # 3. 创建已登录坐席 token(模拟浏览器已有一个坐席在确认授权)
        confirm_token = await _create_agent_token(
            mock_redis, user_id="admin-001", name="管理员"
        )

        # 4. confirm
        confirm_resp = await client.post(
            "/auth_qrcode/confirm",
            json={"ticket": ticket, "otp_code": None},
            headers={"Authorization": f"Bearer {confirm_token}"},
        )

        assert confirm_resp.status_code == 200
        body = confirm_resp.json()
        assert body["code"] == 0
        data = body["data"]
        assert "token" in data
        assert data["employee_id"] == "dev-user-001"
        assert "张三" in data["name"]
        assert data["roles"] == ["agent"]
        # Phase 1.1: 没有传 otp_code,require_otp 应为 False
        assert data["require_otp"] is False

        # 5. 验证 token 写入 Redis(unified format)
        token = data["token"]
        stored = await mock_redis.get(f"user:token:{token}")
        assert stored is not None

    @pytest.mark.asyncio
    async def test_confirm_then_poll_returns_confirmed(self, client, mock_redis):
        """confirm 后 poll → status=confirmed + token 一致。"""
        # create + scan
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]
        await client.post(
            "/auth_qrcode/scan",
            json={"ticket": ticket, "code": "dev:dev-user-001"},
        )

        # confirm
        confirm_token = await _create_agent_token(mock_redis, "admin-001", "管理员")
        confirm_resp = await client.post(
            "/auth_qrcode/confirm",
            json={"ticket": ticket},
            headers={"Authorization": f"Bearer {confirm_token}"},
        )
        new_token = confirm_resp.json()["data"]["token"]

        # poll
        poll_resp = await client.get(f"/auth_qrcode/poll/{ticket}")
        body = poll_resp.json()
        data = body["data"]

        assert data["status"] == "confirmed"
        assert data["token"] == new_token
        assert data["employee_id"] == "dev-user-001"

    @pytest.mark.asyncio
    async def test_confirm_without_auth_returns_unauthorized(self, client, mock_redis):
        """未鉴权 confirm → 401 或 403(FastAPI HTTPBearer 默认 403,本项目统一为 401)。

        这里接受两种状态码是因为 FastAPI HTTPBearer 在不同场景下:
          - 无 Authorization 头 → 403
          - Token 格式错 → 401
        业务上都是"未鉴权",均视为失败。
        """
        # create + scan
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]
        await client.post(
            "/auth_qrcode/scan",
            json={"ticket": ticket, "code": "dev:dev-user-001"},
        )

        # 没带 Authorization 头
        confirm_resp = await client.post(
            "/auth_qrcode/confirm",
            json={"ticket": ticket},
        )

        # 鉴权失败:401 或 403 都接受
        assert confirm_resp.status_code in (401, 403)

    @pytest.mark.asyncio
    async def test_confirm_expired_ticket_fails(self, client, mock_redis):
        """expired ticket(手动 Redis delete 后)confirm → 失败。

        模拟场景: 票据过了 120s,Redis 自动过期。
        这里通过手动 delete qrcode:ticket:{ticket} 模拟。
        """
        # create + scan
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]
        await client.post(
            "/auth_qrcode/scan",
            json={"ticket": ticket, "code": "dev:dev-user-001"},
        )

        # 模拟票据过期: 删除 ticket key
        await mock_redis.delete(f"qrcode:ticket:{ticket}")

        # confirm → 应该失败(1003 资源不存在)
        confirm_token = await _create_agent_token(mock_redis, "admin-001", "管理员")
        confirm_resp = await client.post(
            "/auth_qrcode/confirm",
            json={"ticket": ticket},
            headers={"Authorization": f"Bearer {confirm_token}"},
        )

        assert confirm_resp.status_code == 200
        body = confirm_resp.json()
        assert body["code"] != 0
        assert body["code"] == 1003

    @pytest.mark.asyncio
    async def test_confirm_without_scan_fails(self, client, mock_redis):
        """没扫码(只有 ticket 没有 scan 数据)就 confirm → 失败。"""
        # create 但不 scan
        create_resp = await client.post("/auth_qrcode/create")
        ticket = create_resp.json()["data"]["ticket"]

        confirm_token = await _create_agent_token(mock_redis, "admin-001", "管理员")
        confirm_resp = await client.post(
            "/auth_qrcode/confirm",
            json={"ticket": ticket},
            headers={"Authorization": f"Bearer {confirm_token}"},
        )

        body = confirm_resp.json()
        assert body["code"] != 0
        assert body["code"] == 1003


# --------------------------------------------------------------------------
# 7. 完整端到端流程 smoke test
# --------------------------------------------------------------------------
class TestQrcodeEndToEnd:
    """完整端到端 smoke test。"""

    @pytest.mark.asyncio
    async def test_full_flow(self, client, mock_redis):
        """完整流程: create → poll waiting → scan → poll scanned → confirm → poll confirmed。"""
        # 1. create
        r = await client.post("/auth_qrcode/create")
        ticket = r.json()["data"]["ticket"]
        assert r.json()["code"] == 0

        # 2. poll (waiting)
        r = await client.get(f"/auth_qrcode/poll/{ticket}")
        assert r.json()["data"]["status"] == "waiting"

        # 3. scan
        r = await client.post(
            "/auth_qrcode/scan",
            json={"ticket": ticket, "code": "dev:dev-agent-001"},
        )
        assert r.json()["data"]["success"] is True

        # 4. poll (scanned)
        r = await client.get(f"/auth_qrcode/poll/{ticket}")
        assert r.json()["data"]["status"] == "scanned"
        assert r.json()["data"]["employee_id"] == "dev-agent-001"

        # 5. confirm
        confirm_token = await _create_agent_token(mock_redis, "admin-001", "管理员")
        r = await client.post(
            "/auth_qrcode/confirm",
            json={"ticket": ticket},
            headers={"Authorization": f"Bearer {confirm_token}"},
        )
        new_token = r.json()["data"]["token"]
        assert new_token

        # 6. poll (confirmed + token)
        r = await client.get(f"/auth_qrcode/poll/{ticket}")
        data = r.json()["data"]
        assert data["status"] == "confirmed"
        assert data["token"] == new_token