#!/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
from collections.abc import Callable
from decimal import Decimal, InvalidOperation
from typing import Any

Body = dict[str, Any]
StatusExpectation = int | tuple[int, ...] | list[int] | set[int]

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 as_decimal(value):
    try:
        return Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        return None


def same_id(left, right):
    return str(left) == str(right)


def response_body(data: str) -> Body:
    if not data:
        return {}
    try:
        parsed = json.loads(data)
    except json.JSONDecodeError:
        return {"_raw": data}
    if isinstance(parsed, dict):
        return parsed
    return {"_raw": parsed}


def body_text(body: object):
    if not isinstance(body, dict):
        return str(body)
    if "_raw" in body:
        return str(body["_raw"])
    if "message" in body:
        return str(body["message"])
    if "_error" in body:
        return str(body["_error"])
    return json.dumps(body, ensure_ascii=False)


def has_error_text(body, *parts):
    text = body_text(body)
    return all(part in text for part in parts)


def report_rejected(name, status_code, body, expect_status=(400, 403, 500)):
    report(name, status_code, body, expect_status=expect_status)
    report_check(f"{name} returns error body", bool(body_text(body)), body)


def pick_items(body: object) -> list[Body]:
    if not isinstance(body, dict):
        return []
    if isinstance(body.get("items"), list):
        return [item for item in body["items"] if isinstance(item, dict)]
    if isinstance(body.get("list"), list):
        return [item for item in body["list"] if isinstance(item, dict)]
    return []


def find_item(items: list[Body], predicate: Callable[[Body], bool]) -> Body | None:
    for item in items:
        if predicate(item):
            return item
    return None


def find_item_by_id(items, item_id):
    return find_item(items, lambda item: same_id(item.get("id"), item_id))


def user_from(body: Body) -> Body:
    user = body.get("user") if isinstance(body, dict) else None
    return user if isinstance(user, dict) else {}


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,
    ) -> tuple[int, Body, dict[str, str]]:
        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()
            return resp.status, response_body(data), dict(resp.headers)
        except urllib.error.HTTPError as e:
            data = e.read().decode() if e.fp else ""
            return e.code, response_body(data), dict(e.headers)
        except Exception as e:
            return 0, {"_error": str(e)}, {}

    def get(self, url, **kw) -> tuple[int, Body, dict[str, str]]:
        return self.request("GET", url, **kw)

    def post(self, url, **kw) -> tuple[int, Body, dict[str, str]]:
        return self.request("POST", url, **kw)

    def post_multipart(
        self, url, fields, files, headers=None
    ) -> tuple[int, Body, dict[str, str]]:
        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) -> tuple[int, Body, dict[str, str]]:
        return self.request("PUT", url, **kw)

    def delete(self, url, **kw) -> tuple[int, Body, dict[str, str]]:
        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: StatusExpectation = 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}")


def check_order_status(session, order_id, expected_status, label) -> Body:
    code, body, _ = session.get(f"{GATEWAY}/api/v1/orders/{order_id}")
    report(f"GET /orders/{order_id} ({label})", code, body)
    report_check(
        f"order {order_id} status is {expected_status} ({label})",
        code == 200 and body.get("status") == expected_status,
        body,
    )
    return body


# ============================================================
# 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)
    user = user_from(body)
    report_check(
        f"registered user fields ({label})",
        code == 200
        and as_int(user.get("id")) > 0
        and user.get("username") == username
        and user.get("role") == "consumer"
        and "consumer" in (user.get("verifiedRoles") or []),
        user,
    )
    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")
    user = user_from(body)
    report_check(
        f"login session fields ({label})",
        code == 200
        and bool(jtoken)
        and as_int(user.get("id")) > 0
        and user.get("username") == username,
        {"user": user, "hasJToken": bool(jtoken)},
    )
    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)
    report_check(
        "GET /users/me returns current user",
        code == 200 and same_id(body.get("id"), user_id),
        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)
    report_check(
        "PUT /users/me persists profile fields",
        code == 200
        and same_id(body.get("id"), user_id)
        and body.get("nickname") == "TestNick"
        and body.get("bio") == "testbio",
        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)
    report_check(
        f"GET /users/{user_id} exposes updated public profile",
        code == 200
        and same_id(body.get("id"), user_id)
        and body.get("nickname") == "TestNick"
        and body.get("bio") == "testbio",
        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"))
    report_check(
        "verification applications are pending",
        all(
            find_item(
                pick_items(body),
                lambda item, role=role: (
                    item.get("role") == role and item.get("status") == "pending"
                ),
            )
            for role in ("player", "owner")
        ),
        body,
    )

    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"))
    report_check(
        "reject user verification is pending",
        bool(
            find_item(
                pick_items(body),
                lambda item: (
                    item.get("role") == "owner" and item.get("status") == "pending"
                ),
            )
        ),
        body,
    )

    # 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)
    report_check(
        "rejected verification keeps reason",
        bool(
            find_item(
                pick_items(body),
                lambda item: (
                    item.get("role") == "owner"
                    and item.get("status") == "rejected"
                    and item.get("rejectReason") == "test reject flow"
                ),
            )
        ),
        body,
    )

    code, body, _ = s_reject.post(
        f"{GATEWAY}/api/v1/users/me/switch-role",
        json_body={"role": "owner"},
        headers=csrf_reject,
    )
    report_rejected("POST /users/me/switch-role (rejected owner)", code, body)
    code, body, _ = s_reject.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (after rejected owner switch)", code, body)
    report_check(
        "rejected owner switch does not change current role",
        body.get("role") == "consumer",
        body,
    )

    code, body, _ = s_user.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (after approval)", code, body)
    verified_roles = body.get("verifiedRoles") or []
    verification_status = body.get("verificationStatus") or {}
    report_check(
        "approved roles are visible on current user",
        "player" in verified_roles
        and "owner" in verified_roles
        and verification_status.get("player") == "approved"
        and verification_status.get("owner") == "approved",
        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)
    code, body, _ = s_user.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (after switch player)", code, body)
    report_check("current role is player", body.get("role") == "player", 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},
    )
    report_check(
        "invited user player verification is pending",
        bool(
            find_item(
                pick_items(body),
                lambda item: (
                    item.get("role") == "player" and item.get("status") == "pending"
                ),
            )
        ),
        body,
    )

    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.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (invited user player role)", code, body)
    report_check(
        "invited user current role is player", body.get("role") == "player", 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)
    report_check(
        "invited player profile initialized",
        as_int(body.get("id")) > 0
        and as_int((body.get("user") or {}).get("id")) > 0
        and body.get("status") == "offline",
        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)
    report_check(
        "GET /games returns list shape", isinstance(pick_items(body), list), body
    )

    csrf_admin = s_admin.csrf_headers()
    game_name = f"TestGame_{rand_str(4)}"
    game_icon = "icon.png"
    game_category = "MOBA"
    code, body, _ = s_admin.post(
        f"{GATEWAY}/api/v1/games",
        json_body={
            "name": game_name,
            "icon": game_icon,
            "category": game_category,
        },
        headers=csrf_admin,
    )
    report("POST /games (create)", code, body)
    game_id = body.get("id", 0)
    report_check(
        "created game fields",
        as_int(game_id) > 0
        and body.get("name") == game_name
        and body.get("icon") == game_icon
        and body.get("category") == game_category,
        body,
    )

    code, body2, _ = s.get(f"{GATEWAY}/api/v1/games")
    report("GET /games (after create)", code, body2)
    items = pick_items(body2)
    created_game = find_item_by_id(items, game_id)
    report_check(
        "created game appears in list",
        bool(created_game) and created_game.get("name") == game_name,
        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)
        report_check(
            f"GET /games/{game_id} returns created game",
            code == 200
            and same_id(body.get("id"), game_id)
            and body.get("name") == game_name
            and body.get("category") == game_category,
            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)
    report_check(
        "player profile initialized",
        as_int(player_id) > 0
        and body.get("status") == "offline"
        and as_int((body.get("user") or {}).get("id")) > 0,
        body,
    )

    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)
    report_check(
        "GET /players returns list shape", isinstance(pick_items(body), list), body
    )

    if player_id:
        code, body, _ = s.get(f"{GATEWAY}/api/v1/players/{player_id}")
        report(f"GET /players/{player_id}", code, body)
        report_check(
            f"GET /players/{player_id} returns initialized online player",
            code == 200
            and same_id(body.get("id"), player_id)
            and body.get("status") == "online"
            and body.get("gender") is True
            and as_int(user_from(body).get("id")) > 0
            and isinstance(body.get("services"), list),
            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
        report_check(
            "created service fields",
            code == 200
            and as_int(body.get("id")) > 0
            and same_id(body.get("playerId"), player_id)
            and same_id(body.get("gameId"), game_id)
            and body.get("title") == "Boosting Service"
            and body.get("description") == "Rank boost"
            and as_decimal(body.get("price")) == Decimal("50")
            and body.get("unit") == "game",
            body,
        )

    code, body, _ = s.get(f"{GATEWAY}/api/v1/services")
    report("GET /services", code, body)
    report_check(
        "GET /services returns list shape", isinstance(pick_items(body), list), 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)
        report_check(
            f"GET /services/{service_id} returns created service",
            code == 200
            and same_id(body.get("id"), service_id)
            and body.get("title") == "Boosting Service",
            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)

        code, body, _ = s.get(f"{GATEWAY}/api/v1/services/{service_id}")
        report(f"GET /services/{service_id} (after update)", code, body)
        report_check(
            f"GET /services/{service_id} reflects update",
            code == 200
            and body.get("title") == "Updated Service"
            and as_decimal(body.get("price")) == Decimal("60"),
            body,
        )

        code, body, _ = s.get(f"{GATEWAY}/api/v1/players/{player_id}")
        report(f"GET /players/{player_id} (after service update)", code, body)
        service = find_item_by_id(body.get("services") or [], service_id)
        report_check(
            f"GET /players/{player_id} includes updated service",
            code == 200
            and body.get("gender") is True
            and bool(service)
            and service.get("title") == "Updated Service"
            and as_decimal(service.get("price")) == Decimal("60"),
            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)
        report_check(
            f"GET /players/{player_id}/services contains service",
            bool(find_item_by_id(pick_items(body), service_id)),
            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(),
    )
    code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (owner role)", code, body)
    report_check("current role is owner", body.get("role") == "owner", body)

    csrf = s_owner.csrf_headers()
    shop_name = f"TestShop_{rand_str(4)}"
    code, body, _ = s_owner.post(
        f"{GATEWAY}/api/v1/shops",
        json_body={
            "name": shop_name,
            "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
    report_check(
        "created shop fields",
        as_int(shop_id) > 0
        and body.get("name") == shop_name
        and body.get("description") == "A test shop"
        and body.get("commissionType") == "percentage"
        and body.get("commissionValue") == "10"
        and same_id((body.get("owner") or {}).get("id"), owner_user_id),
        body,
    )

    code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops")
    report("GET /shops", code, body)
    report_check(
        "created shop appears in list",
        bool(find_item_by_id(pick_items(body), shop_id)),
        body,
    )

    if shop_id:
        code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}")
        report(f"GET /shops/{shop_id}", code, body)
        report_check(
            f"GET /shops/{shop_id} returns created shop",
            same_id(body.get("id"), shop_id) and body.get("name") == shop_name,
            body,
        )

        updated_shop_name = f"UpdatedShop_{rand_str(4)}"
        code, body, _ = s_owner.put(
            f"{GATEWAY}/api/v1/shops/{shop_id}",
            json_body={
                "name": updated_shop_name,
                "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)
        report_check(
            f"PUT /shops/{shop_id} returns updated fields",
            same_id(body.get("id"), shop_id)
            and body.get("name") == updated_shop_name
            and body.get("description") == "An updated test shop"
            and body.get("commissionValue") == "12"
            and body.get("allowMultiShop") is True
            and body.get("allowIndependentOrders") is False
            and body.get("dispatchMode") == "manual",
            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)
        report_check(
            f"GET /users/{owner_user_id}/shop returns owned shop",
            same_id(body.get("id"), shop_id)
            and same_id((body.get("owner") or {}).get("id"), owner_user_id),
            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.get(f"{GATEWAY}/api/v1/shops/{shop_id}")
            report(f"GET /shops/{shop_id} (after announcement delete)", code, body)
            report_check(
                "deleted announcement is absent",
                announcement
                not in [str(item) for item in body.get("announcements") or []],
                body,
            )

        template_sections = json.dumps({"layout": "grid", "theme": "dark"})
        code, body, _ = s_owner.put(
            f"{GATEWAY}/api/v1/shops/{shop_id}/template",
            json_body={"sections": template_sections},
            headers=csrf,
        )
        report(f"PUT /shops/{shop_id}/template", code, body)
        code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}")
        report(f"GET /shops/{shop_id} (after template)", code, body)
        report_check(
            "shop template is persisted",
            bool(body.get("templateConfig")),
            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)
        report_check(
            "income stats response has business fields",
            all(
                key in body
                for key in (
                    "monthlyIncome",
                    "pendingSettlement",
                    "totalWithdrawn",
                    "totalOrders",
                    "completedOrders",
                )
            ),
            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},
            )
            invitation = find_item_by_id(pick_items(body), invitation_id)
            report_check(
                "created invitation is pending",
                bool(invitation)
                and same_id(invitation.get("shopId"), shop_id)
                and same_id(invitation.get("playerId"), invited_player_id)
                and invitation.get("status") == "pending"
                and same_id(invitation.get("invitedBy"), owner_user_id),
                body,
            )

            code, body, _ = s_invited_player.post(
                f"{GATEWAY}/api/v1/shops/{shop_id}/invitations",
                json_body={"playerId": invited_player_id},
                headers=s_invited_player.csrf_headers(),
            )
            report_rejected(
                f"POST /shops/{shop_id}/invitations (non-owner)",
                code,
                body,
            )

            if invitation_id:
                code, body, _ = s_owner.post(
                    f"{GATEWAY}/api/v1/shops/invitations/{invitation_id}/accept",
                    json_body={},
                    headers=csrf,
                )
                report_rejected(
                    f"POST /shops/invitations/{invitation_id}/accept (wrong player)",
                    code,
                    body,
                )
                code, body, _ = s_owner.get(
                    f"{GATEWAY}/api/v1/shops/{shop_id}/invitations",
                )
                report(
                    f"GET /shops/{shop_id}/invitations (after wrong accept)", code, body
                )
                pending_invitation = find_item_by_id(pick_items(body), invitation_id)
                report_check(
                    "wrong invitation accept keeps status pending",
                    bool(pending_invitation)
                    and pending_invitation.get("status") == "pending",
                    body,
                )

            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.get(
                    f"{GATEWAY}/api/v1/shops/{shop_id}/invitations",
                )
                report(f"GET /shops/{shop_id}/invitations (after accept)", code, body)
                accepted = find_item_by_id(pick_items(body), invitation_id)
                report_check(
                    "accepted invitation has accepted status",
                    bool(accepted)
                    and accepted.get("status") == "accepted"
                    and as_int(accepted.get("respondedAt")) > 0,
                    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,
                )
                code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}")
                report(f"GET /shops/{shop_id} (after player remove)", code, body)
                report_check(
                    "removed player is reflected in shop profile",
                    as_int(body.get("playerCount")) >= 0,
                    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/{shop_id}/invitations",
                    )
                    report(
                        f"GET /shops/{shop_id}/invitations (after reject)", code, body
                    )
                    rejected = find_item_by_id(pick_items(body), reinvite_id)
                    report_check(
                        "rejected invitation has rejected status",
                        bool(rejected)
                        and rejected.get("status") == "rejected"
                        and as_int(rejected.get("respondedAt")) > 0,
                        body,
                    )

    code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/mine")
    report("GET /shops/mine", code, body)
    report_check(
        "GET /shops/mine returns owned shop", same_id(body.get("id"), shop_id), 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(),
    )
    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (consumer role)", code, body)
    consumer_user_id = pick_user_id(body)
    report_check("current role is consumer", body.get("role") == "consumer", body)

    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
    report_check(
        "created order starts pending payment",
        body.get("ok") is True
        and as_int(order_id) > 0
        and same_id(order_obj.get("consumerId"), consumer_user_id)
        and same_id(order_obj.get("playerId"), player_id)
        and same_id(order_obj.get("shopId"), shop_id)
        and order_obj.get("status") == "pending_payment"
        and as_decimal(order_obj.get("totalPrice")) == Decimal("60")
        and order_obj.get("note") == "test order",
        body,
    )

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders?role=consumer")
    report("GET /orders?role=consumer", code, body)
    report_check(
        "consumer order list contains created order",
        bool(find_item_by_id(pick_items(body), order_id)),
        body,
    )

    code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=player")
    report("GET /orders?role=player", code, body)
    report_check(
        "player order list contains created order",
        bool(find_item_by_id(pick_items(body), order_id)),
        body,
    )

    code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=owner")
    report("GET /orders?role=owner", code, body)
    report_check(
        "owner order list contains created order",
        bool(find_item_by_id(pick_items(body), order_id)),
        body,
    )

    if order_id:
        body = check_order_status(
            s_consumer, order_id, "pending_payment", "after create"
        )
        report_check(
            "created order detail matches participants",
            same_id(body.get("consumerId"), consumer_user_id)
            and same_id(body.get("playerId"), player_id)
            and same_id(body.get("shopId"), shop_id)
            and as_decimal(body.get("totalPrice")) == Decimal("60")
            and body.get("note") == "test order",
            body,
        )

        code, body, _ = s_actor.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/accept",
            json_body={},
            headers=s_actor.csrf_headers(),
        )
        report_rejected(f"POST /orders/{order_id}/accept (before pay)", code, body)
        check_order_status(
            s_consumer, order_id, "pending_payment", "after early accept"
        )

        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/confirm-close",
            json_body={},
            headers=csrf,
        )
        report_rejected(
            f"POST /orders/{order_id}/confirm-close (before close request)", code, body
        )
        check_order_status(
            s_consumer, order_id, "pending_payment", "after early confirm-close"
        )

        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)
        check_order_status(s_consumer, order_id, "pending_accept", "after pay")

        code, body, _ = s_actor.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/request-close",
            json_body={},
            headers=s_actor.csrf_headers(),
        )
        report_rejected(
            f"POST /orders/{order_id}/request-close (before accept)", code, body
        )
        check_order_status(
            s_consumer, order_id, "pending_accept", "after early request-close"
        )

        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/pay",
            json_body={},
            headers=csrf,
        )
        report_rejected(f"POST /orders/{order_id}/pay (second time)", code, body)
        check_order_status(
            s_consumer, order_id, "pending_accept", "after duplicate pay"
        )

        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)
        body = check_order_status(s_consumer, order_id, "in_progress", "after accept")
        report_check(
            "accepted order has acceptedAt", bool(body.get("acceptedAt")), body
        )

        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order_id}/confirm-close",
            json_body={},
            headers=csrf,
        )
        report_rejected(
            f"POST /orders/{order_id}/confirm-close (before request-close)", code, body
        )
        check_order_status(
            s_consumer, order_id, "in_progress", "after early confirm-close"
        )

        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)
        check_order_status(s_consumer, order_id, "pending_close", "after request-close")

        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)
        check_order_status(
            s_consumer, order_id, "pending_review", "after confirm-close"
        )

    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
    report_check(
        "second order starts pending payment",
        as_int(order2_id) > 0 and order2.get("status") == "pending_payment",
        body,
    )

    if order2_id:
        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order2_id}/review",
            json_body={"rating": 5, "content": "too early"},
            headers=csrf,
        )
        report_rejected(
            f"POST /orders/{order2_id}/review (before completion)", code, body
        )
        check_order_status(
            s_consumer, order2_id, "pending_payment", "after early review"
        )

    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)
        reordered = body.get("order", {}) if isinstance(body, dict) else {}
        report_check(
            "reorder creates a new pending order",
            body.get("ok") is True
            and as_int(reordered.get("id")) > 0
            and not same_id(reordered.get("id"), order_id)
            and reordered.get("status") == "pending_payment",
            body,
        )

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

        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)
        check_order_status(
            s_consumer, order2_id, "cancelled", "second order after cancel"
        )

        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/orders/{order2_id}/pay",
            json_body={},
            headers=csrf,
        )
        report_rejected(f"POST /orders/{order2_id}/pay (after cancel)", code, body)
        check_order_status(s_consumer, order2_id, "cancelled", "after cancelled pay")

    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)
    paid_order = body.get("order", {}) if isinstance(body, dict) else {}
    report_check(
        "create+pay order starts pending accept",
        body.get("ok") is True
        and as_int(paid_order.get("id")) > 0
        and paid_order.get("status") == "pending_accept",
        body,
    )

    return order_id


def phase8b_review(
    s_consumer: Session,
    s_actor: Session,
    order_id,
    consumer_user_id,
    player_user_id,
):
    print("\n=== Phase 8b: Reviews ===")
    if not order_id:
        skip("Review flow", "No pending_review order id")
        return

    csrf = s_consumer.csrf_headers()
    code, body, _ = s_consumer.post(
        f"{GATEWAY}/api/v1/orders/{order_id}/review",
        json_body={"rating": 5, "content": "great service"},
        headers=csrf,
    )
    report(f"POST /orders/{order_id}/review", code, body)

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}/reviews")
    report(f"GET /orders/{order_id}/reviews", code, body)
    if code == 200:
        items = pick_items(body)
        report_check(
            f"GET /orders/{order_id}/reviews shape",
            isinstance(items, list) and isinstance(body.get("meta"), dict),
            body,
        )
        report_check(
            f"GET /orders/{order_id}/reviews hides sealed review",
            len(items) == 0,
            body,
        )

    code, body, _ = s_actor.post(
        f"{GATEWAY}/api/v1/orders/{order_id}/review",
        json_body={"rating": 4, "content": "smooth buyer"},
        headers=s_actor.csrf_headers(),
    )
    report(f"POST /orders/{order_id}/review (counterparty)", code, body)

    order_body = check_order_status(
        s_consumer, order_id, "completed", "after two reviews"
    )
    report_check(
        "completed order has completedAt",
        bool(order_body.get("completedAt")),
        order_body,
    )

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}/reviews")
    report(f"GET /orders/{order_id}/reviews (after two reviews)", code, body)
    reviews = pick_items(body)
    report_check(
        "two-sided reviews are visible and unsealed",
        len(reviews) == 2
        and all(same_id(item.get("orderId"), order_id) for item in reviews)
        and all(item.get("sealed") is False for item in reviews)
        and bool(
            find_item(reviews, lambda item: item.get("content") == "great service")
        )
        and bool(
            find_item(reviews, lambda item: item.get("content") == "smooth buyer")
        ),
        body,
    )

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/reviews?limit=20")
    report("GET /reviews?limit=20", code, body)
    report_check(
        "public review list contains unsealed review",
        bool(
            find_item(
                pick_items(body),
                lambda item: (
                    same_id(item.get("orderId"), order_id)
                    and item.get("sealed") is False
                ),
            )
        ),
        body,
    )

    if player_user_id:
        code, body, _ = s_consumer.get(
            f"{GATEWAY}/api/v1/users/{player_user_id}/reviews?limit=20",
        )
        report(f"GET /users/{player_user_id}/reviews?limit=20", code, body)
        report_check(
            "player received review is listed",
            bool(
                find_item(
                    pick_items(body),
                    lambda item: (
                        same_id(item.get("orderId"), order_id)
                        and same_id(item.get("fromUserId"), consumer_user_id)
                        and item.get("content") == "great service"
                    ),
                )
            ),
            body,
        )


def phase8c_dispute(
    s_consumer: Session, s_actor: Session, player_id, service_id, shop_id
):
    print("\n=== Phase 8c: Disputes ===")
    if not player_id or not service_id:
        skip("Dispute flow", "Missing player or service id")
        return 0

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (dispute consumer)", code, body)
    consumer_user_id = pick_user_id(body)

    code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (dispute actor)", code, body)
    actor_user_id = pick_user_id(body)

    csrf = s_consumer.csrf_headers()
    payload = {
        "playerId": player_id,
        "serviceId": service_id,
        "quantity": 1,
        "note": "test dispute order",
    }
    if shop_id:
        payload["shopId"] = shop_id

    code, body, _ = s_consumer.post(
        f"{GATEWAY}/api/v1/orders",
        json_body=payload,
        headers=csrf,
    )
    report("POST /orders (create for dispute)", code, body)
    order_obj = body.get("order", {}) if isinstance(body, dict) else {}
    order_id = order_obj.get("id", 0) if isinstance(order_obj, dict) else 0
    report_check(
        "dispute order starts pending payment",
        as_int(order_id) > 0 and order_obj.get("status") == "pending_payment",
        body,
    )
    if not order_id:
        skip("Dispute flow", "No order id returned")
        return 0

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

    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 (dispute flow)", code, body)
    check_order_status(
        s_consumer, order_id, "in_progress", "dispute order after accept"
    )

    code, body, _ = s_consumer.post(
        f"{GATEWAY}/api/v1/orders/{order_id}/dispute",
        json_body={
            "reason": "test dispute reason",
            "evidence": ["http://example.com/evidence.jpg"],
        },
        headers=csrf,
    )
    report(f"POST /orders/{order_id}/dispute", code, body)
    check_order_status(s_consumer, order_id, "disputed", "after dispute create")

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}/dispute")
    report(f"GET /orders/{order_id}/dispute", code, body)
    dispute_id = as_int(body.get("id")) if isinstance(body, dict) else 0
    report_check(
        "created dispute fields",
        dispute_id > 0
        and same_id(body.get("orderId"), order_id)
        and same_id(body.get("initiatorId"), consumer_user_id)
        and same_id(body.get("respondentId"), actor_user_id)
        and body.get("reason") == "test dispute reason"
        and body.get("evidence") == ["http://example.com/evidence.jpg"]
        and body.get("status") == "open",
        body,
    )

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/disputes")
    report("GET /disputes", code, body)
    report_check(
        "dispute list contains created dispute",
        bool(find_item_by_id(pick_items(body), dispute_id)),
        body,
    )

    code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/disputes?status=open")
    report("GET /disputes?status=open", code, body)
    report_check(
        "open dispute list contains created open dispute",
        bool(
            find_item(
                pick_items(body),
                lambda item: (
                    same_id(item.get("id"), dispute_id) and item.get("status") == "open"
                ),
            )
        ),
        body,
    )

    if dispute_id:
        code, body, _ = s_actor.post(
            f"{GATEWAY}/api/v1/disputes/{dispute_id}/response",
            json_body={
                "reason": "test respondent response",
                "evidence": ["http://example.com/response.jpg"],
            },
            headers=s_actor.csrf_headers(),
        )
        report(
            f"POST /disputes/{dispute_id}/response",
            code,
            body,
        )
        code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}/dispute")
        report(f"GET /orders/{order_id}/dispute (after response)", code, body)
        report_check(
            "responded dispute fields",
            body.get("status") == "reviewing"
            and body.get("respondentReason") == "test respondent response"
            and body.get("respondentEvidence") == ["http://example.com/response.jpg"],
            body,
        )

        code, body, _ = s_consumer.post(
            f"{GATEWAY}/api/v1/disputes/{dispute_id}/appeal",
            json_body={"reason": "test appeal guard"},
            headers=csrf,
        )
        report(
            f"POST /disputes/{dispute_id}/appeal (expect status check)",
            code,
            body,
            expect_status=(400, 403, 500),
        )

    return dispute_id


def phase8d_notifications(s: Session):
    print("\n=== Phase 8d: Notifications ===")
    code, body, _ = s.get(f"{GATEWAY}/api/v1/notifications")
    report("GET /notifications", code, body)
    report_check(
        "GET /notifications returns list shape",
        code == 200
        and isinstance(pick_items(body), list)
        and isinstance(body.get("meta"), dict),
        body,
    )

    items = pick_items(body) if code == 200 and isinstance(body, dict) else []
    if items:
        notification_id = as_int(items[0].get("id"))
        if notification_id:
            code, body, _ = s.put(
                f"{GATEWAY}/api/v1/notifications/{notification_id}/read",
                json_body={},
                headers=s.csrf_headers(),
            )
            report(f"PUT /notifications/{notification_id}/read", code, body)
            code, body, _ = s.get(f"{GATEWAY}/api/v1/notifications")
            report("GET /notifications (after single read)", code, body)
            notification = find_item_by_id(pick_items(body), notification_id)
            report_check(
                "single notification is marked read",
                bool(notification) and notification.get("read") is True,
                body,
            )
    else:
        skip("PUT /notifications/:id/read", "No notification item returned")

    code, body, _ = s.put(
        f"{GATEWAY}/api/v1/notifications/read-all",
        json_body={},
        headers=s.csrf_headers(),
    )
    report("PUT /notifications/read-all", code, body)
    code, body, _ = s.get(f"{GATEWAY}/api/v1/notifications")
    report("GET /notifications (after read-all)", code, body)
    items = pick_items(body)
    report_check(
        "all returned notifications are read",
        all(item.get("read") is True for item in items),
        body,
    )


def phase8e_search_and_favorites(s: Session, user_id, player_id, shop_id):
    print("\n=== Phase 8e: Search & Favorites ===")

    code, body, _ = s.get(f"{GATEWAY}/api/v1/search?q=LOL&limit=10")
    report("GET /search?q=LOL&limit=10", code, body)
    if code == 200:
        report_check(
            "GET /search response shape",
            isinstance(pick_items(body), list) and isinstance(body.get("meta"), dict),
            body,
        )

    code, body, _ = s.get(f"{GATEWAY}/api/v1/recommendations/home?limit=10")
    report("GET /recommendations/home?limit=10", code, body)
    if code == 200:
        report_check(
            "GET /recommendations/home response shape",
            isinstance(pick_items(body), list) and isinstance(body.get("meta"), dict),
            body,
        )

    code, body, _ = s.get(f"{GATEWAY}/api/v1/favorites")
    report("GET /favorites", code, body)
    report_check(
        "GET /favorites returns list shape",
        code == 200
        and isinstance(pick_items(body), list)
        and isinstance(body.get("meta"), dict),
        body,
    )

    if not player_id or not user_id:
        skip("Favorites mutation flow", "Missing user or player id")
        return

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/favorites",
        json_body={"targetType": "player", "targetId": "not-a-snowflake"},
        headers=s.csrf_headers(),
    )
    report_rejected("POST /favorites (invalid targetId)", code, body)
    code, body, _ = s.get(f"{GATEWAY}/api/v1/favorites")
    report("GET /favorites (after invalid targetId)", code, body)
    report_check(
        "invalid favorite target is absent",
        not bool(
            find_item(
                pick_items(body),
                lambda item: (
                    item.get("targetType") == "player"
                    and same_id(item.get("targetId"), "not-a-snowflake")
                ),
            )
        ),
        body,
    )

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/favorites",
        json_body={"targetType": "player", "targetId": str(player_id)},
        headers=s.csrf_headers(),
    )
    report("POST /favorites (player)", code, body)

    code, body, _ = s.get(
        f"{GATEWAY}/api/v1/users/{user_id}/favorites/check"
        f"?targetType=player&targetId={player_id}",
    )
    report(f"GET /users/{user_id}/favorites/check (player)", code, body)
    if code == 200:
        report_check("favorite check after add", body.get("favorited") is True, body)

    if shop_id:
        code, body, _ = s.post(
            f"{GATEWAY}/api/v1/favorites",
            json_body={"targetType": "shop", "targetId": str(shop_id)},
            headers=s.csrf_headers(),
        )
        report("POST /favorites (shop)", code, body)

    code, body, _ = s.get(f"{GATEWAY}/api/v1/favorites")
    report("GET /favorites (after add)", code, body)
    favorite_items = pick_items(body)
    report_check(
        "player favorite is listed",
        bool(
            find_item(
                favorite_items,
                lambda item: (
                    item.get("targetType") == "player"
                    and same_id(item.get("targetId"), player_id)
                    and same_id(item.get("userId"), user_id)
                ),
            )
        ),
        body,
    )
    if shop_id:
        report_check(
            "shop favorite is listed",
            bool(
                find_item(
                    favorite_items,
                    lambda item: (
                        item.get("targetType") == "shop"
                        and same_id(item.get("targetId"), shop_id)
                        and same_id(item.get("userId"), user_id)
                    ),
                )
            ),
            body,
        )
    favorite_id = 0
    for item in favorite_items:
        if item.get("targetType") == "player" and str(item.get("targetId")) == str(
            player_id
        ):
            favorite_id = as_int(item.get("id"))
            break
    report_check("locate favorite id", bool(favorite_id), {"id": favorite_id})

    if favorite_id:
        code, body, _ = s.delete(
            f"{GATEWAY}/api/v1/favorites/{favorite_id}",
            headers=s.csrf_headers(),
        )
        report(f"DELETE /favorites/{favorite_id}", code, body)

        code, body, _ = s.get(
            f"{GATEWAY}/api/v1/users/{user_id}/favorites/check"
            f"?targetType=player&targetId={player_id}",
        )
        report(f"GET /users/{user_id}/favorites/check (after delete)", code, body)
        if code == 200:
            report_check(
                "favorite check after delete", body.get("favorited") is False, body
            )
        code, body, _ = s.get(f"{GATEWAY}/api/v1/favorites")
        report("GET /favorites (after delete)", code, body)
        report_check(
            "deleted player favorite is absent",
            not bool(
                find_item(
                    pick_items(body),
                    lambda item: (
                        item.get("targetType") == "player"
                        and same_id(item.get("targetId"), player_id)
                    ),
                )
            ),
            body,
        )


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)
    initial_balance = as_decimal(body.get("balance")) or Decimal("0")
    report_check(
        "wallet balance response has numeric fields",
        as_decimal(body.get("balance")) is not None
        and as_decimal(body.get("frozenBalance")) is not None,
        body,
    )

    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/wallet/withdraw",
        json_body={"amount": "0", "method": "alipay"},
        headers=csrf,
    )
    report_rejected("POST /wallet/withdraw (zero amount)", code, body)
    code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/balance")
    report("GET /wallet/balance (after zero withdraw)", code, body)
    report_check(
        "zero withdraw keeps balance unchanged",
        as_decimal(body.get("balance")) == initial_balance,
        body,
    )

    excessive_amount = initial_balance + Decimal("999999")
    code, body, _ = s.post(
        f"{GATEWAY}/api/v1/wallet/withdraw",
        json_body={"amount": str(excessive_amount), "method": "alipay"},
        headers=csrf,
    )
    report_rejected("POST /wallet/withdraw (insufficient balance)", code, body)
    code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/balance")
    report("GET /wallet/balance (after insufficient withdraw)", code, body)
    report_check(
        "insufficient withdraw keeps balance unchanged",
        as_decimal(body.get("balance")) == initial_balance,
        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.get(f"{GATEWAY}/api/v1/wallet/balance")
    report("GET /wallet/balance (after topup)", code, body)
    topup_balance = as_decimal(body.get("balance"))
    report_check(
        "wallet balance increases after topup",
        topup_balance == initial_balance + Decimal("100"),
        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/balance")
    report("GET /wallet/balance (after withdraw)", code, body)
    withdraw_balance = as_decimal(body.get("balance"))
    report_check(
        "wallet balance decreases after withdraw",
        topup_balance is not None and withdraw_balance == topup_balance - Decimal("10"),
        body,
    )

    code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/transactions")
    report("GET /wallet/transactions", code, body)
    transactions = pick_items(body)
    report_check(
        "wallet transactions include topup and withdrawal",
        bool(
            find_item(
                transactions,
                lambda item: (
                    item.get("type") == "topup"
                    and as_decimal(item.get("amount")) == Decimal("100")
                    and item.get("description") == "topup via alipay"
                ),
            )
        )
        and bool(
            find_item(
                transactions,
                lambda item: (
                    item.get("type") == "withdrawal"
                    and as_decimal(item.get("amount")) == Decimal("10")
                    and item.get("description") == "withdraw via alipay"
                ),
            )
        ),
        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)
    report_check(
        "created post fields",
        as_int(post_id) > 0
        and body.get("title") == "Test Post"
        and body.get("content") == "Hello world"
        and body.get("tags") == ["test"]
        and body.get("pinned") is False
        and as_int(body.get("likeCount")) == 0
        and as_int(body.get("commentCount")) == 0
        and body.get("liked") is False
        and same_id((body.get("author") or {}).get("id"), user_id),
        body,
    )

    code, body, _ = s.get(f"{GATEWAY}/api/v1/posts")
    report("GET /posts", code, body)
    report_check(
        "created post appears in list",
        bool(find_item_by_id(pick_items(body), post_id)),
        body,
    )

    if post_id:
        code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}")
        report(f"GET /posts/{post_id}", code, body)
        report_check(
            f"GET /posts/{post_id} returns created post",
            same_id(body.get("id"), post_id) and body.get("title") == "Test Post",
            body,
        )
        original_like_count = as_int(body.get("likeCount"))

        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.get(f"{GATEWAY}/api/v1/posts/{post_id}")
        report(f"GET /posts/{post_id} (after like)", code, body)
        report_check(
            "post like state is visible",
            body.get("liked") is True
            and as_int(body.get("likeCount")) >= original_like_count + 1,
            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.get(f"{GATEWAY}/api/v1/posts/{post_id}")
        report(f"GET /posts/{post_id} (after unlike)", code, body)
        report_check(
            "post unlike state is visible",
            body.get("liked") is False
            and as_int(body.get("likeCount")) == original_like_count,
            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.get(f"{GATEWAY}/api/v1/posts/{post_id}")
        report(f"GET /posts/{post_id} (after pin)", code, body)
        report_check("post pin state is visible", body.get("pinned") is True, 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.get(f"{GATEWAY}/api/v1/posts/{post_id}")
        report(f"GET /posts/{post_id} (after unpin)", code, body)
        report_check("post unpin state is visible", body.get("pinned") is False, 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)
        report_check(
            "created comment fields",
            as_int(comment_id) > 0
            and body.get("content") == "Nice post!"
            and same_id((body.get("author") or {}).get("id"), user_id)
            and as_int(body.get("likeCount")) == 0
            and body.get("liked") is False,
            body,
        )

        code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}/comments")
        report(f"GET /posts/{post_id}/comments", code, body)
        report_check(
            "created comment appears in list",
            bool(find_item_by_id(pick_items(body), comment_id)),
            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.get(f"{GATEWAY}/api/v1/posts/{post_id}/comments")
            report(f"GET /posts/{post_id}/comments (after comment like)", code, body)
            comment = find_item_by_id(pick_items(body), comment_id)
            report_check(
                "comment like state is visible",
                bool(comment)
                and comment.get("liked") is True
                and as_int(comment.get("likeCount")) >= 1,
                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/posts/{post_id}/comments")
            report(f"GET /posts/{post_id}/comments (after comment unlike)", code, body)
            comment = find_item_by_id(pick_items(body), comment_id)
            report_check(
                "comment unlike state is visible",
                bool(comment)
                and comment.get("liked") is False
                and as_int(comment.get("likeCount")) == 0,
                body,
            )

    code, body, _ = s.get(f"{GATEWAY}/api/v1/users/{user_id}/posts")
    report(f"GET /users/{user_id}/posts", code, body)
    report_check(
        "user posts contain created post",
        bool(find_item_by_id(pick_items(body), post_id)),
        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"empty-{rand_str(4)}.txt", b"", "text/plain")},
        headers=s.csrf_headers(),
    )
    report_rejected("POST /upload (empty file)", code, body)
    report_check("empty upload does not return url", not body.get("url"), body)

    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",
            isinstance(body.get("url"), str) and body.get("url", "").startswith("http"),
            {"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)
    report_check(
        "email verification send returns request id",
        code == 200
        and bool(body.get("requestId"))
        and as_int(body.get("expireInSec")) > 0,
        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)
    report_check(
        "forgot password send returns request id",
        code == 200
        and bool(body.get("requestId"))
        and as_int(body.get("expireInSec")) > 0,
        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)
    code, body, _ = s.get(f"{GATEWAY}/api/v1/users/me")
    report("GET /users/me (after logout)", code, body, expect_status=(401, 403))


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)
    report_check(
        "reset password allows login with new password",
        code == 200 and bool(s_login.get_cookie("JToken")) and pick_user_id(body) > 0,
        {"userId": pick_user_id(body), "hasJToken": bool(s_login.get_cookie("JToken"))},
    )


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)
    code, body, _ = s.get(f"{GATEWAY}/api/v1/services")
    report("GET /services (after delete)", code, body)
    report_check(
        "deleted service is absent from service list",
        not bool(find_item_by_id(pick_items(body), service_id)),
        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)

    order_id = phase8_order(s_consumer, s_user, player_id, service_id, shop_id)
    phase8b_review(s_consumer, s_user, order_id, consumer_user_id, user_id)
    phase8c_dispute(s_consumer, s_user, player_id, service_id, shop_id)
    phase8d_notifications(s_consumer)
    phase8e_search_and_favorites(s_consumer, consumer_user_id, player_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()
