#!/usr/bin/env python3
"""Full API integration test for juwan-backend.

Runs against the local dev docker-compose environment.
Gateway: http://127.0.0.1:18080
Reads verification codes from local Redis only for dev registration flow.
"""

import json
import random
import string
import sys
import urllib.request
import urllib.error
import urllib.parse
import http.cookiejar
import os
import subprocess

GATEWAY = "http://127.0.0.1:18080"
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
REDIS_CONTAINER = os.getenv("REDIS_CONTAINER", "juwan-redis")

passed = 0
failed = 0
errors_list = []


def rand_str(n=8):
    return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))


def as_int(value, default=0):
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def pick_items(body):
    if isinstance(body.get("items"), list):
        return body["items"]
    if isinstance(body.get("list"), list):
        return body["list"]
    return []


def pick_user_id(body):
    if not isinstance(body, dict):
        return 0
    if isinstance(body.get("user"), dict):
        return as_int(body["user"].get("id"))
    return as_int(body.get("id"))


def read_vcode_from_redis(request_id, scene, account):
    if not request_id:
        return ""

    result = subprocess.run(
        [
            "docker",
            "exec",
            REDIS_CONTAINER,
            "redis-cli",
            "GET",
            f"vcode:{request_id}:{scene}:{account}",
        ],
        capture_output=True,
        text=True,
        timeout=5,
    )
    return result.stdout.strip()


def build_multipart_form(fields, files):
    boundary = f"----juwan{rand_str(12)}"
    chunks = []

    for name, value in fields.items():
        chunks.extend(
            [
                f"--{boundary}\r\n".encode(),
                f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode(),
                str(value).encode(),
                b"\r\n",
            ]
        )

    for name, (filename, content, content_type) in files.items():
        if isinstance(content, str):
            content = content.encode()
        chunks.extend(
            [
                f"--{boundary}\r\n".encode(),
                (
                    f'Content-Disposition: form-data; name="{name}"; '
                    f'filename="{filename}"\r\n'
                ).encode(),
                f"Content-Type: {content_type}\r\n\r\n".encode(),
                content,
                b"\r\n",
            ]
        )

    chunks.append(f"--{boundary}--\r\n".encode())
    body = b"".join(chunks)
    return f"multipart/form-data; boundary={boundary}", body


class Session:
    """Minimal cookie-aware HTTP session using stdlib only."""

    def __init__(self):
        self.cookie_jar = http.cookiejar.CookieJar(
            policy=http.cookiejar.DefaultCookiePolicy(
                secure_protocols=("https", "http")
            )
        )
        self.opener = urllib.request.build_opener(
            urllib.request.HTTPCookieProcessor(self.cookie_jar)
        )

    def get_cookie(self, name):
        for c in self.cookie_jar:
            if c.name == name:
                return c.value
        return None

    def request(
        self,
        method,
        url,
        json_body=None,
        headers=None,
        form_data=None,
        raw_body=None,
    ):
        hdrs = headers or {}
        body = None
        if json_body is not None:
            body = json.dumps(json_body).encode()
            hdrs.setdefault("Content-Type", "application/json")
        elif form_data is not None:
            body = urllib.parse.urlencode(form_data).encode()
            hdrs.setdefault("Content-Type", "application/x-www-form-urlencoded")
        elif raw_body is not None:
            body = raw_body

        req = urllib.request.Request(url, data=body, headers=hdrs, method=method)
        try:
            resp = self.opener.open(req, timeout=15)
            data = resp.read().decode()
            try:
                return resp.status, json.loads(data) if data else {}, dict(resp.headers)
            except json.JSONDecodeError:
                return resp.status, {"_raw": data}, dict(resp.headers)
        except urllib.error.HTTPError as e:
            data = e.read().decode() if e.fp else ""
            try:
                return e.code, json.loads(data) if data else {}, dict(e.headers)
            except json.JSONDecodeError:
                return e.code, {"_raw": data}, dict(e.headers)
        except Exception as e:
            return 0, {"_error": str(e)}, {}

    def get(self, url, **kw):
        return self.request("GET", url, **kw)

    def post(self, url, **kw):
        return self.request("POST", url, **kw)

    def post_multipart(self, url, fields, files, headers=None):
        hdrs = dict(headers or {})
        content_type, body = build_multipart_form(fields, files)
        hdrs["Content-Type"] = content_type
        return self.post(url, headers=hdrs, raw_body=body)

    def put(self, url, **kw):
        return self.request("PUT", url, **kw)

    def delete(self, url, **kw):
        return self.request("DELETE", url, **kw)

    def csrf_headers(self):
        token = self.get_cookie("__Host-XSRF-TOKEN")
        return {"xsrf-token": token} if token else {}


def report(name, status_code, body, expect_status=200):
    global passed, failed
    if isinstance(expect_status, (list, tuple, set)):
        ok = status_code in expect_status
    else:
        ok = status_code == expect_status
    mark = "PASS" if ok else "FAIL"
    if not ok:
        failed += 1
        errors_list.append((name, status_code, body))
    else:
        passed += 1
    body_preview = json.dumps(body, ensure_ascii=False)
    if len(body_preview) > 200:
        body_preview = body_preview[:200] + "..."
    print(f"  [{mark}] {name}: HTTP {status_code} {body_preview}")
    return ok


def report_check(name, ok, body=None):
    global passed, failed
    mark = "PASS" if ok else "FAIL"
    if not ok:
        failed += 1
        errors_list.append((name, "CHECK", body))
    else:
        passed += 1
    body_preview = json.dumps(body, ensure_ascii=False)
    if len(body_preview) > 200:
        body_preview = body_preview[:200] + "..."
    print(f"  [{mark}] {name}: {body_preview}")
    return ok


def skip(name, reason):
    print(f"  [SKIP] {name}: {reason}")


# ============================================================
# Phase 0: Health check & CSRF
# ============================================================
def phase0_health(s: Session):
    print("\n=== Phase 0: Health & CSRF ===")
    code, body, _ = s.get(f"{GATEWAY}/healthz")
    report("GET /healthz", code, body)
    xsrf = s.get_cookie("__Host-XSRF-TOKEN")
    xsrf_guard = s.get_cookie("__Host-XSRF-GUARD")
    print(f"  XSRF-TOKEN: {xsrf}")
    print(f"  XSRF-GUARD: {xsrf_guard}")
    if not xsrf:
        print("  [WARN] No XSRF-TOKEN cookie received, POST requests will fail")


# ============================================================
# Phase 1: Registration
# ============================================================
def phase1_register(s: Session, username, email, password, label="user"):
    print(f"\n=== Phase 1: Register ({label}) ===")

    # Step 1: send verification code via gateway
    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/email/verification-code/send",
        json_body={"email": email, "scene": "register"},
        headers=s.csrf_headers(),
    )
    report(f"POST /email/verification-code/send ({label})", code, body)
    request_id = body.get("requestId", "")
    if not request_id:
        print("  [ERROR] No requestId returned, cannot register")
        return None

    # Step 2: read the dev verification code from Redis
    print("  [BYPASS] Reading vcode from Redis...")
    vcode = read_vcode_from_redis(request_id, "register", email)
    if not vcode:
        print("  [WARN] No verification code found in Redis")
    else:
        print(f"  Vcode from Redis: {vcode}")

    # Step 3: register via gateway with X-Request-Id
    csrf = s.csrf_headers()
    csrf["X-Request-Id"] = request_id
    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/auth/register",
        json_body={
            "username": username,
            "email": email,
            "password": password,
            "vcode": vcode,
        },
        headers=csrf,
    )
    report(f"POST /auth/register ({label})", code, body)
    return body


def phase1_login(s: Session, username, password, label="user"):
    print(f"\n=== Phase 1b: Login ({label}) ===")
    csrf = s.csrf_headers()
    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/auth/login",
        json_body={"username": username, "password": password},
        headers=csrf,
    )
    report(f"POST /auth/login ({label})", code, body)
    jtoken = s.get_cookie("JToken")
    print(f"  JToken: {jtoken[:30]}..." if jtoken else "  JToken: None")
    return body


# ============================================================
# Phase 2: User endpoints (authenticated)
# ============================================================
def phase2_user(s: Session, user_id):
    print("\n=== Phase 2: User Endpoints ===")
    csrf = s.csrf_headers()

    code, body, _ = s.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me", code, body)

    code, body, _ = s.put(
        f"{GATEWAY}/api/v1/users/me",
        json_body={"nickname": "TestNick", "bio": "testbio"},
        headers=csrf,
    )
    report("PUT /users/me", code, body)

    code, body, _ = s.put(
        f"{GATEWAY}/api/v1/users/me/preferences/notifications",
        json_body={"order": True, "community": True, "system": True},
        headers=csrf,
    )
    report("PUT /users/me/preferences/notifications", code, body)

    code, body, _ = s.put(
        f"{GATEWAY}/api/v1/users/me/preferences/theme",
        json_body={"theme": "dark"},
        headers=csrf,
    )
    report("PUT /users/me/preferences/theme", code, body)

    code, body, _ = s.get(f"{GATEWAY}/api/v1/users/{user_id}")
    report(f"GET /users/{user_id} (public)", code, body)


# ============================================================
# Phase 3: Built-in Admin + Verification flow
# ============================================================
def phase3_admin_and_verification(
    s_admin: Session,
    s_user: Session,
    s_reject: Session,
    admin_user,
    admin_pass,
):
    print("\n=== Phase 3: Built-in Admin & Verification ===")

    # Login admin via gateway
    s_admin.get(f"{GATEWAY}/healthz")  # get CSRF
    csrf = s_admin.csrf_headers()
    code, body, _ = s_admin.post(
        f"{GATEWAY}/api/v1/auth/login",
        json_body={"username": admin_user, "password": admin_pass},
        headers=csrf,
    )
    report("POST /auth/login (admin)", code, body)

    code, body, _ = s_admin.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (admin)", code, body)
    admin_id = pick_user_id(body)

    # User applies for player verification
    csrf_user = s_user.csrf_headers()
    code, body, _ = s_user.post(
        f"{GATEWAY}/api/v1/users/me/verification",
        json_body={
            "role": "player",
            "materials": {
                "idCardFront": "http://example.com/front.jpg",
                "idCardBack": "http://example.com/back.jpg",
                "gameScreenshots": ["http://example.com/ss1.jpg"],
                "voiceDemo": "http://example.com/voice.mp3",
            },
        },
        headers=csrf_user,
    )
    report("POST /users/me/verification (apply player)", code, body)

    # User applies for owner verification
    code, body, _ = s_user.post(
        f"{GATEWAY}/api/v1/users/me/verification",
        json_body={
            "role": "owner",
            "materials": {
                "idCardFront": "http://example.com/front.jpg",
                "idCardBack": "http://example.com/back.jpg",
                "gameScreenshots": [],
                "voiceDemo": "",
            },
        },
        headers=csrf_user,
    )
    report("POST /users/me/verification (apply owner)", code, body)

    # Another user applies so admin reject flow is covered too
    csrf_reject = s_reject.csrf_headers()
    code, body, _ = s_reject.post(
        f"{GATEWAY}/api/v1/users/me/verification",
        json_body={
            "role": "owner",
            "materials": {
                "idCardFront": "http://example.com/reject-front.jpg",
                "idCardBack": "http://example.com/reject-back.jpg",
                "gameScreenshots": [],
                "voiceDemo": "",
            },
        },
        headers=csrf_reject,
    )
    report("POST /users/me/verification (apply owner, reject user)", code, body)

    # Get my verifications
    code, body, _ = s_user.get(f"{GATEWAY}/api/v1/users/me/verification")
    report("GET /users/me/verification", code, body)
    user_verification_ids = {}
    for v in pick_items(body):
        user_verification_ids[v.get("role")] = as_int(v.get("id"))

    code, body, _ = s_reject.get(f"{GATEWAY}/api/v1/users/me/verification")
    report("GET /users/me/verification (reject user)", code, body)
    reject_verification_ids = {}
    for v in pick_items(body):
        reject_verification_ids[v.get("role")] = as_int(v.get("id"))

    # Admin: list and approve via gateway
    code, body, _ = s_admin.get(f"{GATEWAY}/api/v1/admin/verifications")
    report("GET /admin/verifications", code, body)

    # Admin: approve only the current test user's records
    for role in ("player", "owner"):
        vid = user_verification_ids.get(role)
        if not vid:
            continue
        code, body, _ = s_admin.post(
            f"{GATEWAY}/api/v1/admin/verifications/{vid}/approve",
            json_body={},
            headers=s_admin.csrf_headers(),
        )
        report(
            f"POST /admin/verifications/{vid}/approve ({role})",
            code,
            body,
        )

    reject_vid = reject_verification_ids.get("owner")
    if reject_vid:
        code, body, _ = s_admin.post(
            f"{GATEWAY}/api/v1/admin/verifications/{reject_vid}/reject",
            json_body={"reason": "test reject flow"},
            headers=s_admin.csrf_headers(),
        )
        report(
            f"POST /admin/verifications/{reject_vid}/reject (owner)",
            code,
            body,
        )

    code, body, _ = s_reject.get(f"{GATEWAY}/api/v1/users/me/verification")
    report("GET /users/me/verification (after reject)", code, body)

    # User: switch role to player
    code, body, _ = s_user.post(
        f"{GATEWAY}/api/v1/users/me/switch-role",
        json_body={"role": "player"},
        headers=csrf_user,
    )
    report("POST /users/me/switch-role (player)", code, body)

    return admin_id


def phase3b_secondary_player(s_admin: Session, s_player: Session):
    print("\n=== Phase 3b: Secondary Player Setup ===")

    csrf_player = s_player.csrf_headers()
    code, body, _ = s_player.post(
        f"{GATEWAY}/api/v1/users/me/verification",
        json_body={
            "role": "player",
            "materials": {
                "idCardFront": "http://example.com/player-front.jpg",
                "idCardBack": "http://example.com/player-back.jpg",
                "gameScreenshots": ["http://example.com/player-ss1.jpg"],
                "voiceDemo": "http://example.com/player-voice.mp3",
            },
        },
        headers=csrf_player,
    )
    report("POST /users/me/verification (apply player, invited user)", code, body)

    code, body, _ = s_player.get(f"{GATEWAY}/api/v1/users/me/verification")
    report("GET /users/me/verification (invited user)", code, body)
    verification_id = 0
    for item in pick_items(body):
        if item.get("role") == "player":
            verification_id = as_int(item.get("id"))
            break
    report_check(
        "lookup player verification id (invited user)",
        bool(verification_id),
        {"id": verification_id},
    )

    if verification_id:
        code, body, _ = s_admin.post(
            f"{GATEWAY}/api/v1/admin/verifications/{verification_id}/approve",
            json_body={},
            headers=s_admin.csrf_headers(),
        )
        report(
            f"POST /admin/verifications/{verification_id}/approve (invited user player)",
            code,
            body,
        )

    code, body, _ = s_player.post(
        f"{GATEWAY}/api/v1/users/me/switch-role",
        json_body={"role": "player"},
        headers=s_player.csrf_headers(),
    )
    report("POST /users/me/switch-role (invited user player)", code, body)

    code, body, _ = s_player.post(
        f"{GATEWAY}/api/v1/players/me",
        json_body={},
        headers=s_player.csrf_headers(),
    )
    report("POST /players/me (invited user init)", code, body)
    return as_int(body.get("id"))


def phase4_follow(s: Session, target_user_id):
    print("\n=== Phase 4: Follow/Unfollow ===")
    csrf = s.csrf_headers()

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/users/{target_user_id}/follow",
        json_body={},
        headers=csrf,
    )
    report(f"POST /users/{target_user_id}/follow", code, body)

    code, body, _ = s.delete(
        f"{GATEWAY}/api/v1/users/{target_user_id}/follow",
        headers=csrf,
    )
    report(f"DELETE /users/{target_user_id}/follow", code, body)


def phase5_games(s: Session, s_admin: Session):
    print("\n=== Phase 5: Games ===")

    code, body, _ = s.get(f"{GATEWAY}/api/v1/games")
    report("GET /games", code, body)

    csrf_admin = s_admin.csrf_headers()
    code, body, _ = s_admin.post(
        f"{GATEWAY}/api/v1/games",
        json_body={
            "name": f"TestGame_{rand_str(4)}",
            "icon": "icon.png",
            "category": "MOBA",
        },
        headers=csrf_admin,
    )
    report("POST /games (create)", code, body)
    game_id = body.get("id", 0)

    code, body2, _ = s.get(f"{GATEWAY}/api/v1/games")
    report("GET /games (after create)", code, body2)
    items = pick_items(body2)
    if not game_id and items:
        game_id = as_int(items[-1].get("id"))

    if game_id:
        code, body, _ = s.get(f"{GATEWAY}/api/v1/games/{game_id}")
        report(f"GET /games/{game_id}", code, body)

    return game_id


def phase6_player(s: Session, game_id):
    print("\n=== Phase 6: Player ===")
    csrf = s.csrf_headers()

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/players/me",
        json_body={},
        headers=csrf,
    )
    report("POST /players/me (init)", code, body)
    player_id = body.get("id", 0)

    code, body, _ = s.put(
        f"{GATEWAY}/api/v1/players/me/status",
        json_body={"status": "online"},
        headers=csrf,
    )
    report("PUT /players/me/status", code, body)

    code, body, _ = s.get(f"{GATEWAY}/api/v1/players")
    report("GET /players", code, body)

    if player_id:
        code, body, _ = s.get(f"{GATEWAY}/api/v1/players/{player_id}")
        report(f"GET /players/{player_id}", code, body)

    svc_body = None
    if game_id:
        code, body, _ = s.post(
            f"{GATEWAY}/api/v1/services",
            json_body={
                "gameId": game_id,
                "title": "Boosting Service",
                "description": "Rank boost",
                "price": 50.0,
                "unit": "game",
            },
            headers=csrf,
        )
        report("POST /services (create)", code, body)
        svc_body = body

    code, body, _ = s.get(f"{GATEWAY}/api/v1/services")
    report("GET /services", code, body)

    service_id = svc_body.get("id", 0) if svc_body else 0
    if service_id:
        code, body, _ = s.get(f"{GATEWAY}/api/v1/services/{service_id}")
        report(f"GET /services/{service_id}", code, body)

        code, body, _ = s.put(
            f"{GATEWAY}/api/v1/services/{service_id}",
            json_body={
                "title": "Updated Service",
                "price": 60.0,
                "availability": ["weekday"],
            },
            headers=csrf,
        )
        report(f"PUT /services/{service_id}", code, body)

    if player_id:
        code, body, _ = s.get(f"{GATEWAY}/api/v1/players/{player_id}/services")
        report(f"GET /players/{player_id}/services", code, body)

    return player_id, service_id


def phase7_shop(
    s_owner: Session, s_invited_player: Session, owner_user_id, invited_player_id
):
    print("\n=== Phase 7: Shop ===")

    s_owner.post(
        f"{GATEWAY}/api/v1/users/me/switch-role",
        json_body={"role": "owner"},
        headers=s_owner.csrf_headers(),
    )

    csrf = s_owner.csrf_headers()
    code, body, _ = s_owner.post(
        f"{GATEWAY}/api/v1/shops",
        json_body={
            "name": f"TestShop_{rand_str(4)}",
            "description": "A test shop",
            "commissionType": "percentage",
            "commissionValue": "10",
        },
        headers=csrf,
    )
    report("POST /shops (create)", code, body)
    shop_id_str = body.get("id", "0")
    try:
        shop_id = int(shop_id_str)
    except (ValueError, TypeError):
        shop_id = 0

    code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops")
    report("GET /shops", code, body)

    if shop_id:
        code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}")
        report(f"GET /shops/{shop_id}", code, body)

        code, body, _ = s_owner.put(
            f"{GATEWAY}/api/v1/shops/{shop_id}",
            json_body={
                "name": f"UpdatedShop_{rand_str(4)}",
                "description": "An updated test shop",
                "commissionType": "percentage",
                "commissionValue": "12",
                "allowMultiShop": True,
                "allowIndependentOrders": False,
                "dispatchMode": "manual",
            },
            headers=csrf,
        )
        report(f"PUT /shops/{shop_id}", code, body)

        code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/users/{owner_user_id}/shop")
        report(f"GET /users/{owner_user_id}/shop", code, body)

        announcement = f"Grand opening {rand_str(4)}!"
        code, body, _ = s_owner.post(
            f"{GATEWAY}/api/v1/shops/{shop_id}/announcements",
            json_body={"content": announcement},
            headers=csrf,
        )
        report(f"POST /shops/{shop_id}/announcements", code, body)

        code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}")
        report(f"GET /shops/{shop_id} (after announcement)", code, body)
        announcement_index = -1
        announcements = body.get("announcements") if isinstance(body, dict) else None
        if isinstance(announcements, list):
            for idx in range(len(announcements) - 1, -1, -1):
                if announcement in str(announcements[idx]):
                    announcement_index = idx
                    break
        report_check(
            f"locate announcement index ({shop_id})",
            announcement_index >= 0,
            {"index": announcement_index},
        )
        if announcement_index >= 0:
            code, body, _ = s_owner.delete(
                f"{GATEWAY}/api/v1/shops/{shop_id}/announcements/{announcement_index}",
                headers=csrf,
            )
            report(
                f"DELETE /shops/{shop_id}/announcements/{announcement_index}",
                code,
                body,
            )

        code, body, _ = s_owner.put(
            f"{GATEWAY}/api/v1/shops/{shop_id}/template",
            json_body={"sections": json.dumps({"layout": "grid", "theme": "dark"})},
            headers=csrf,
        )
        report(f"PUT /shops/{shop_id}/template", code, body)

        code, body, _ = s_owner.get(
            f"{GATEWAY}/api/v1/shops/{shop_id}/income-stats",
        )
        report(f"GET /shops/{shop_id}/income-stats", code, body)

        if invited_player_id:
            code, body, _ = s_owner.post(
                f"{GATEWAY}/api/v1/shops/{shop_id}/invitations",
                json_body={"playerId": invited_player_id},
                headers=csrf,
            )
            report(f"POST /shops/{shop_id}/invitations", code, body)

            code, body, _ = s_owner.get(
                f"{GATEWAY}/api/v1/shops/{shop_id}/invitations",
            )
            report(f"GET /shops/{shop_id}/invitations", code, body)

            invitation_id = 0
            for item in pick_items(body):
                if as_int(item.get("playerId")) == invited_player_id:
                    invitation_id = as_int(item.get("id"))
                    break
            report_check(
                "locate invitation id",
                bool(invitation_id),
                {"id": invitation_id},
            )

            s_invited_player.post(
                f"{GATEWAY}/api/v1/users/me/switch-role",
                json_body={"role": "player"},
                headers=s_invited_player.csrf_headers(),
            )
            code, body, _ = s_invited_player.get(
                f"{GATEWAY}/api/v1/shops/invitations/mine",
            )
            report("GET /shops/invitations/mine", code, body)

            if invitation_id:
                inv_csrf = s_invited_player.csrf_headers()
                code, body, _ = s_invited_player.post(
                    f"{GATEWAY}/api/v1/shops/invitations/{invitation_id}/accept",
                    json_body={},
                    headers=inv_csrf,
                )
                report(
                    f"POST /shops/invitations/{invitation_id}/accept",
                    code,
                    body,
                )

                code, body, _ = s_owner.delete(
                    f"{GATEWAY}/api/v1/shops/{shop_id}/players/{invited_player_id}",
                    headers=csrf,
                )
                report(
                    f"DELETE /shops/{shop_id}/players/{invited_player_id}",
                    code,
                    body,
                )

                # Reject invitation flow: re-invite the removed player, then reject
                code, body, _ = s_owner.post(
                    f"{GATEWAY}/api/v1/shops/{shop_id}/invitations",
                    json_body={"playerId": invited_player_id},
                    headers=csrf,
                )
                report(f"POST /shops/{shop_id}/invitations (re-invite)", code, body)

                code, body, _ = s_owner.get(
                    f"{GATEWAY}/api/v1/shops/{shop_id}/invitations",
                )
                reinvite_id = 0
                for item in pick_items(body):
                    if (
                        as_int(item.get("playerId")) == invited_player_id
                        and item.get("status") == "pending"
                    ):
                        reinvite_id = as_int(item.get("id"))
                        break
                if reinvite_id:
                    code, body, _ = s_invited_player.delete(
                        f"{GATEWAY}/api/v1/shops/invitations/{reinvite_id}",
                        headers=s_invited_player.csrf_headers(),
                    )
                    report(
                        f"DELETE /shops/invitations/{reinvite_id} (reject)",
                        code,
                        body,
                    )

    code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/mine")
    report("GET /shops/mine", code, body)

    return shop_id


def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, shop_id):
    print("\n=== Phase 8: Orders ===")

    s_consumer.post(
        f"{GATEWAY}/api/v1/users/me/switch-role",
        json_body={"role": "consumer"},
        headers=s_consumer.csrf_headers(),
    )

    csrf = s_consumer.csrf_headers()
    code, body, _ = s_consumer.post(
        f"{GATEWAY}/api/v1/orders",
        json_body={
            "playerId": player_id,
            "serviceId": service_id,
            "shopId": shop_id,
            "quantity": 1,
            "note": "test order",
        },
        headers=csrf,
    )
    report("POST /orders (create)", code, body)
    order_obj = body.get("order", {})
    order_id = order_obj.get("id", 0) if isinstance(order_obj, dict) else 0

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders?role=consumer")
    report("GET /orders?role=consumer", code, body)

    code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=player")
    report("GET /orders?role=player", code, body)

    code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=owner")
    report("GET /orders?role=owner", code, body)

    if order_id:
        code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}")
        report(f"GET /orders/{order_id}", code, body)

        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/pay",
            json_body={},
            headers=csrf,
        )
        report(f"POST /orders/{order_id}/pay", code, body)

        code, body, _ = s_actor.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/accept",
            json_body={},
            headers=s_actor.csrf_headers(),
        )
        report(f"POST /orders/{order_id}/accept", code, body)

        code, body, _ = s_actor.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/request-close",
            json_body={},
            headers=s_actor.csrf_headers(),
        )
        report(f"POST /orders/{order_id}/request-close", code, body)

        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/confirm-close",
            json_body={},
            headers=csrf,
        )
        report(f"POST /orders/{order_id}/confirm-close", code, body)

    code, body, _ = s_consumer.post(
        f"{GATEWAY}/api/v1/orders",
        json_body={
            "playerId": player_id,
            "serviceId": service_id,
            "quantity": 1,
        },
        headers=csrf,
    )
    order2 = body.get("order", {})
    order2_id = order2.get("id", 0) if isinstance(order2, dict) else 0

    if order_id:
        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/reorder",
            json_body={},
            headers=csrf,
        )
        report(f"POST /orders/{order_id}/reorder", code, body)

    if order2_id:
        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order2_id}/cancel",
            json_body={},
            headers=csrf,
        )
        report(f"POST /orders/{order2_id}/cancel", code, body)

    code, body, _ = s_consumer.post(
        f"{GATEWAY}/api/v1/orders/paid",
        json_body={
            "playerId": player_id,
            "serviceId": service_id,
            "quantity": 1,
        },
        headers=csrf,
    )
    report("POST /orders/paid (create+pay)", code, body)

    return order_id


def phase9_wallet(s: Session):
    print("\n=== Phase 9: Wallet ===")
    csrf = s.csrf_headers()

    code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/balance")
    report("GET /wallet/balance", code, body)

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/wallet/topup",
        json_body={"amount": "100.00", "method": "alipay"},
        headers=csrf,
    )
    report("POST /wallet/topup", code, body)

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/wallet/withdraw",
        json_body={"amount": "10.00", "method": "alipay"},
        headers=csrf,
    )
    report("POST /wallet/withdraw", code, body)

    code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/transactions")
    report("GET /wallet/transactions", code, body)


def phase10_community(s: Session, user_id):
    print("\n=== Phase 10: Community ===")
    csrf = s.csrf_headers()

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/posts",
        json_body={
            "title": "Test Post",
            "content": "Hello world",
            "images": [],
            "tags": ["test"],
        },
        headers=csrf,
    )
    report("POST /posts (create)", code, body)
    post_id = body.get("id", 0)

    code, body, _ = s.get(f"{GATEWAY}/api/v1/posts")
    report("GET /posts", code, body)

    if post_id:
        code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}")
        report(f"GET /posts/{post_id}", code, body)

        code, body, _ = s.post(
            f"{GATEWAY}/api/v1/posts/{post_id}/like",
            json_body={},
            headers=csrf,
        )
        report(f"POST /posts/{post_id}/like", code, body)

        code, body, _ = s.delete(
            f"{GATEWAY}/api/v1/posts/{post_id}/like",
            headers=csrf,
        )
        report(f"DELETE /posts/{post_id}/like", code, body)

        code, body, _ = s.post(
            f"{GATEWAY}/api/v1/posts/{post_id}/pin",
            json_body={},
            headers=csrf,
        )
        report(f"POST /posts/{post_id}/pin", code, body)

        code, body, _ = s.delete(
            f"{GATEWAY}/api/v1/posts/{post_id}/pin",
            headers=csrf,
        )
        report(f"DELETE /posts/{post_id}/pin", code, body)

        code, body, _ = s.post(
            f"{GATEWAY}/api/v1/posts/{post_id}/comments",
            json_body={"content": "Nice post!"},
            headers=csrf,
        )
        report(f"POST /posts/{post_id}/comments", code, body)
        comment_id = body.get("id", 0)

        code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}/comments")
        report(f"GET /posts/{post_id}/comments", code, body)

        if comment_id:
            code, body, _ = s.post(
                f"{GATEWAY}/api/v1/comments/{comment_id}/like",
                json_body={},
                headers=csrf,
            )
            report(f"POST /comments/{comment_id}/like", code, body)

            code, body, _ = s.delete(
                f"{GATEWAY}/api/v1/comments/{comment_id}/like",
                headers=csrf,
            )
            report(f"DELETE /comments/{comment_id}/like", code, body)

    code, body, _ = s.get(f"{GATEWAY}/api/v1/users/{user_id}/posts")
    report(f"GET /users/{user_id}/posts", code, body)

    return post_id


def phase11_objectstory(s: Session):
    print("\n=== Phase 11: Objectstory (File) ===")

    code, body, _ = s.post_multipart(
        f"{GATEWAY}/api/v1/upload",
        fields={"type": "post"},
        files={
            "file": (
                f"test-{rand_str(4)}.txt",
                f"juwan-objectstory-{rand_str(8)}",
                "text/plain",
            )
        },
        headers=s.csrf_headers(),
    )
    report("POST /upload", code, body)
    if code == 200:
        report_check(
            "POST /upload returned url",
            bool(body.get("url")),
            {"url": body.get("url", "")},
        )

    code, body, _ = s.get(f"{GATEWAY}/api/v1/files?key=nonexistent")
    report(
        "GET /files?key=nonexistent (expect error)",
        code,
        body,
        expect_status=(400, 500),
    )

    return


def phase12_email(s: Session):
    print("\n=== Phase 12: Email ===")
    csrf = s.csrf_headers()

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/email/verification-code/send",
        json_body={"email": f"test_{rand_str(4)}@example.com", "scene": "register"},
        headers=csrf,
    )
    report("POST /email/verification-code/send (gateway)", code, body)

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/auth/forgot-password/send",
        json_body={"email": f"test_{rand_str(4)}@example.com"},
        headers=csrf,
    )
    report("POST /auth/forgot-password/send (gateway)", code, body)


def phase13_logout(s: Session):
    print("\n=== Phase 13: Logout ===")
    csrf = s.csrf_headers()
    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/auth/logout",
        json_body={},
        headers=csrf,
    )
    report("POST /auth/logout", code, body)


def phase14_reset_password(username, email, new_password):
    print("\n=== Phase 14: Forgot/Reset Password ===")

    s_reset = Session()
    s_reset.get(f"{GATEWAY}/healthz")
    csrf = s_reset.csrf_headers()

    code, body, _ = s_reset.post(
        f"{GATEWAY}/api/v1/auth/forgot-password/send",
        json_body={"email": email},
        headers=csrf,
    )
    report("POST /auth/forgot-password/send", code, body)
    request_id = body.get("requestId", "")
    if not request_id:
        print("  [WARN] No requestId returned, skip reset-password")
        return

    vcode = read_vcode_from_redis(request_id, "reset_password", email)
    if not vcode:
        print("  [WARN] No reset password verification code found in Redis")
        return

    csrf["X-Request-Id"] = request_id
    code, body, _ = s_reset.post(
        f"{GATEWAY}/api/v1/auth/reset-password",
        json_body={
            "email": email,
            "vcode": vcode,
            "newPassword": new_password,
        },
        headers=csrf,
    )
    report("POST /auth/reset-password", code, body)
    if code != 200:
        print("  [SKIP] Reset password failed, skip login verification")
        return

    s_login = Session()
    s_login.get(f"{GATEWAY}/healthz")
    code, body, _ = s_login.post(
        f"{GATEWAY}/api/v1/auth/login",
        json_body={"username": username, "password": new_password},
        headers=s_login.csrf_headers(),
    )
    report("POST /auth/login (after reset)", code, body)


def phase15_player_service_delete(s: Session, service_id):
    print("\n=== Phase 15: Delete Service ===")
    if not service_id:
        print("  [SKIP] No service to delete")
        return
    csrf = s.csrf_headers()
    code, body, _ = s.delete(
        f"{GATEWAY}/api/v1/services/{service_id}",
        headers=csrf,
    )
    report(f"DELETE /services/{service_id}", code, body)


def main():
    global passed, failed

    suffix = rand_str(6)
    user1_name = f"testuser_{suffix}"
    user1_email = f"testuser_{suffix}@example.com"
    user1_pass = "TestPass123!"
    consumer_name = f"consumer_{suffix}"
    consumer_email = f"consumer_{suffix}@example.com"
    consumer_pass = "ConsumerPass123!"
    consumer_new_pass = "ConsumerPass456!"
    admin_name = ADMIN_USERNAME
    admin_pass = ADMIN_PASSWORD

    print(f"Test run: user={user1_name}, admin={admin_name}")

    s_user = Session()
    s_admin = Session()
    s_consumer = Session()

    phase0_health(s_user)
    phase1_register(s_user, user1_name, user1_email, user1_pass, label="primary")
    login_resp = phase1_login(s_user, user1_name, user1_pass, label="primary")

    user_id = pick_user_id(login_resp)
    print(f"  User ID: {user_id}")

    phase2_user(s_user, user_id)

    s_consumer.get(f"{GATEWAY}/healthz")
    phase1_register(
        s_consumer, consumer_name, consumer_email, consumer_pass, label="consumer"
    )
    consumer_login_resp = phase1_login(
        s_consumer,
        consumer_name,
        consumer_pass,
        label="consumer",
    )
    consumer_user_id = pick_user_id(consumer_login_resp)
    print(f"  Consumer User ID: {consumer_user_id}")

    admin_id = phase3_admin_and_verification(
        s_admin,
        s_user,
        s_consumer,
        admin_name,
        admin_pass,
    )
    invited_player_id = phase3b_secondary_player(s_admin, s_consumer)

    if admin_id and user_id:
        phase4_follow(s_user, admin_id)

    s_user.post(
        f"{GATEWAY}/api/v1/users/me/switch-role",
        json_body={"role": "player"},
        headers=s_user.csrf_headers(),
    )

    game_id = phase5_games(s_user, s_admin)
    player_id, service_id = phase6_player(s_user, game_id)
    shop_id = phase7_shop(s_user, s_consumer, user_id, invited_player_id)

    phase8_order(s_consumer, s_user, player_id, service_id, shop_id)
    phase9_wallet(s_user)
    phase10_community(s_user, user_id)
    phase11_objectstory(s_user)
    phase12_email(s_user)
    phase14_reset_password(consumer_name, consumer_email, consumer_new_pass)
    phase15_player_service_delete(s_user, service_id)
    phase13_logout(s_user)

    print(f"\n{'=' * 60}")
    print(f"RESULTS: {passed} passed, {failed} failed, {passed + failed} total")
    print(f"{'=' * 60}")
    if errors_list:
        print("\nFailed tests:")
        for name, status, body in errors_list:
            body_s = json.dumps(body, ensure_ascii=False)
            if len(body_s) > 300:
                body_s = body_s[:300] + "..."
            print(f"  - {name}: HTTP {status} {body_s}")

    sys.exit(1 if failed else 0)


if __name__ == "__main__":
    main()
