/* @vitest-environment jsdom */

import { act, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';

import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
import type {
  CustomWorldAgentSessionSnapshot,
  CustomWorldWorkSummary,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import type {
  BabyObjectMatchDraft,
  CreateBabyObjectMatchDraftRequest,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type {
  JumpHopRuntimeRunSnapshotResponse,
  JumpHopWorkDetailResponse,
  JumpHopWorkProfileResponse,
  JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
  PuzzleAnchorPack,
  PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type {
  CreatePuzzleAgentSessionRequest,
  PuzzleAgentSessionSnapshot,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
import type {
  CustomWorldGalleryCard,
  CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import type {
  WoodenFishGalleryCardResponse,
  WoodenFishWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/woodenFish';
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import {
  readPublicWorkCodeFromLocationSearch,
  resolveSelectionStageFromPath,
} from '../../routing/appPageRoutes';
import { ApiClientError } from '../../services/apiClient';
import type { AuthUser } from '../../services/authService';
import {
  createBarkBattleDraft,
  deleteBarkBattleWork,
  generateAllBarkBattleImageAssets,
  listBarkBattleGallery,
  listBarkBattleWorks,
  publishBarkBattleWork,
  updateBarkBattleDraftConfig,
} from '../../services/bark-battle-creation';
import {
  createBigFishCreationSession,
  getBigFishCreationSession,
} from '../../services/big-fish-creation';
import { listBigFishGallery } from '../../services/big-fish-gallery';
import {
  recordBigFishPlay,
  startBigFishRun,
  submitBigFishInput,
} from '../../services/big-fish-runtime';
import { listBigFishWorks } from '../../services/big-fish-works';
import {
  type CreationEntryConfig,
  fetchCreationEntryConfig,
  type UnifiedCreationSpec,
} from '../../services/creationEntryConfigService';
import {
  cancelCreativeAgentSession,
  confirmCreativePuzzleTemplate,
  createCreativeAgentSession,
  streamCreativeAgentMessage,
  streamCreativeDraftEdit,
} from '../../services/creative-agent';
import {
  createBabyObjectMatchDraft,
  deleteLocalBabyObjectMatchDraft,
  listLocalBabyObjectMatchDrafts,
  publishBabyObjectMatchWork,
  regenerateBabyObjectMatchDraftAssets,
  saveBabyObjectMatchDraft,
} from '../../services/edutainment-baby-object';
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
import { match3dCreationClient } from '../../services/match3d-creation';
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
import {
  deleteMatch3DWork,
  getMatch3DWorkDetail,
  listMatch3DGallery,
  listMatch3DWorks,
} from '../../services/match3d-works';
import * as match3dGeneratedModelCache from '../../services/match3dGeneratedModelCache';
import {
  createPuzzleAgentSession,
  executePuzzleAgentAction,
  getPuzzleAgentSession,
} from '../../services/puzzle-agent';
import {
  getPuzzleGalleryDetail,
  listPuzzleGallery,
  remixPuzzleGalleryWork,
} from '../../services/puzzle-gallery';
import {
  generatePuzzleOnboardingWork,
  savePuzzleOnboardingWork,
} from '../../services/puzzle-onboarding';
import {
  advancePuzzleNextLevel,
  dragPuzzlePieceOrGroup,
  getPuzzleRun,
  startPuzzleRun,
  submitPuzzleLeaderboard,
  swapPuzzlePieces,
  updatePuzzleRunPause,
  usePuzzleRuntimeProp,
} from '../../services/puzzle-runtime';
import {
  dragLocalPuzzlePiece,
  startLocalPuzzleRun,
  swapLocalPuzzlePieces,
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
import { listPuzzleWorks, updatePuzzleWork } from '../../services/puzzle-works';
import {
  createRpgCreationSession,
  executeRpgCreationAction,
  getRpgCreationOperation,
  getRpgCreationResultView,
  getRpgCreationSession,
  listRpgCreationWorks,
  streamRpgCreationMessage,
  upsertRpgWorldProfile,
} from '../../services/rpg-creation';
import {
  clearRpgProfileBrowseHistory as clearProfileBrowseHistory,
  getRpgEntryWorldGalleryDetail,
  getRpgProfileDashboard as getProfileDashboard,
  listRpgEntryWorldGallery,
  listRpgEntryWorldLibrary,
  listRpgProfileBrowseHistory as listProfileBrowseHistory,
  listRpgProfileSaveArchives as listProfileSaveArchives,
  resumeRpgProfileSaveArchive as resumeProfileSaveArchive,
  upsertRpgProfileBrowseHistory as upsertProfileBrowseHistory,
} from '../../services/rpg-entry';
import {
  deleteRpgEntryWorldProfile,
  getRpgEntryWorldGalleryDetail as getRpgEntryWorldGalleryDetailFromClient,
  getRpgEntryWorldGalleryDetailByCode,
  likeRpgEntryWorldGallery,
  recordRpgEntryWorldGalleryPlay,
  remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import { squareHoleCreationClient } from '../../services/square-hole-creation';
import {
  dropSquareHoleShape,
  finishSquareHoleTimeUp,
  restartSquareHoleRun,
  startSquareHoleRun,
  stopSquareHoleRun,
} from '../../services/square-hole-runtime';
import {
  deleteSquareHoleWork,
  getSquareHoleWorkDetail,
  listSquareHoleGallery,
  listSquareHoleWorks,
} from '../../services/square-hole-works';
import { listVisualNovelGallery } from '../../services/visual-novel-runtime';
import { listVisualNovelWorks } from '../../services/visual-novel-works';
import { woodenFishClient } from '../../services/wooden-fish/woodenFishClient';
import { type CustomWorldProfile, WorldType } from '../../types';
import {
  AuthUiContext,
  type PlatformSettingsSection,
} from '../auth/AuthUiContext';
import {
  getUnifiedCreationSpec,
  type UnifiedCreationPlayId,
} from '../unified-creation/unifiedCreationSpecs';
import {
  RpgEntryFlowShell,
  type RpgEntryFlowShellProps,
  type SelectionStage,
} from './RpgEntryFlowShell';

const authServiceMocks = vi.hoisted(() => ({
  ensureRuntimeGuestToken: vi.fn(async () => ({
    token: 'runtime-guest-token',
    expiresAt: '2099-01-01T00:00:00.000Z',
  })),
  isWechatMiniProgramWebViewRuntime: vi.fn(() => false),
  getPublicAuthUserByCode: vi.fn(
    async (publicUserCode: string): Promise<PublicUserSummary> => ({
      id: `public-user-${publicUserCode}`,
      publicUserCode,
      username: 'author_user',
      displayName: '公开作者',
      avatarUrl: null,
    }),
  ),
  getPublicAuthUserById: vi.fn(
    async (userId: string): Promise<PublicUserSummary> => ({
      id: userId,
      publicUserCode: `code-${userId}`,
      username: 'author_user',
      displayName: '公开作者',
      avatarUrl: null,
    }),
  ),
}));

vi.mock('../../services/authService', () => ({
  ensureRuntimeGuestToken: authServiceMocks.ensureRuntimeGuestToken,
  isWechatMiniProgramWebViewRuntime:
    authServiceMocks.isWechatMiniProgramWebViewRuntime,
  getPublicAuthUserByCode: authServiceMocks.getPublicAuthUserByCode,
  getPublicAuthUserById: authServiceMocks.getPublicAuthUserById,
}));

async function clickFirstButtonByName(
  user: ReturnType<typeof userEvent.setup>,
  name: string | RegExp,
) {
  const buttons = await screen.findAllByRole('button', { name });
  await user.click(buttons[0]!);
}

async function clickFirstAsyncButtonByName(
  user: ReturnType<typeof userEvent.setup>,
  name: string | RegExp,
) {
  const buttons = await screen.findAllByRole('button', { name });
  await user.click(buttons[0]!);
}

async function openCreateTemplateHub(user: ReturnType<typeof userEvent.setup>) {
  await clickFirstButtonByName(user, '创作');
  const panel = getPlatformTabPanel('create');
  await waitFor(() => {
    expect(panel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(
    await within(panel).findByRole('tablist', { name: '创作入口页签' }),
  ).toBeTruthy();
  // 中文注释：真实最近创作存在时会成为默认页签，模板入口用例需显式切回模板分类。
  if (!within(panel).queryByRole('button', { name: /拼图/u })) {
    await user.click(
      await within(panel).findByRole('tab', { name: '热门推荐' }),
    );
  }
  expect(
    await within(panel).findByRole('button', { name: /拼图/u }),
  ).toBeTruthy();
  expect(within(panel).queryByText('拼图工作区：missing-session')).toBeNull();
  return panel;
}

async function findCreationTypeButton(name: string | RegExp) {
  const matcher =
    typeof name === 'string'
      ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u')
      : name;
  return within(getPlatformTabPanel('create')).findByRole('button', {
    name: matcher,
  });
}

function queryCreationTypeButton(name: string | RegExp) {
  const matcher =
    typeof name === 'string'
      ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u')
      : name;
  return within(getPlatformTabPanel('create')).queryByRole('button', {
    name: matcher,
  });
}

async function openPuzzleFormFromCreateHub(
  user: ReturnType<typeof userEvent.setup>,
) {
  await user.click(await findCreationTypeButton('拼图'));
  await screen.findByText(/拼图工作区：/u);
}

async function openDraftHub(user: ReturnType<typeof userEvent.setup>) {
  await clickFirstButtonByName(user, '草稿');
  const panel = getPlatformTabPanel('saves');
  await waitFor(() => {
    expect(panel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(await within(panel).findByRole('tab', { name: /全部/u })).toBeTruthy();
}

async function expectDraftHubGeneratingBadgeCountAtLeast(count: number) {
  const panel = getPlatformTabPanel('saves');
  await waitFor(() => {
    expect(
      within(panel).getAllByLabelText('生成中').length,
    ).toBeGreaterThanOrEqual(count);
  });
}

async function openDiscoverHub(user: ReturnType<typeof userEvent.setup>) {
  await clickFirstButtonByName(user, '发现');
  const panel = getPlatformTabPanel('category');
  await waitFor(() => {
    expect(panel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(
    await within(panel).findByPlaceholderText('搜索作品号、名称、作者、描述'),
  ).toBeTruthy();
  return panel;
}

async function openProfilePlayedWorks(
  user: ReturnType<typeof userEvent.setup>,
) {
  await clickFirstButtonByName(user, '我的');
  await user.click(
    await screen.findByRole('button', { name: /已玩游戏数量/u }),
  );
  expect(await screen.findByRole('dialog', { name: '玩过' })).toBeTruthy();
  expect(await screen.findByText('可继续')).toBeTruthy();
}

async function openExistingRpgDraft(
  user: ReturnType<typeof userEvent.setup>,
  actionName: string | RegExp = /继续(?:完善|创作)/u,
) {
  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: actionName }));
}

const ISOLATED_RUNTIME_AUTH_OPTIONS = {
  authImpact: 'local',
  skipRefresh: true,
  notifyAuthStateChange: false,
  clearAuthOnUnauthorized: false,
};
const LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS = ISOLATED_RUNTIME_AUTH_OPTIONS;

function getPlatformTabPanel(tab: string) {
  const panel = document.getElementById(`platform-tab-panel-${tab}`);
  if (!panel) {
    throw new Error(`Missing platform tab panel: ${tab}`);
  }

  return panel;
}

async function findPlatformTabPanel(tab: string) {
  await waitFor(() => {
    expect(document.getElementById(`platform-tab-panel-${tab}`)).toBeTruthy();
  });
  return getPlatformTabPanel(tab);
}

function buildTestUnifiedCreationSpec(
  playId: UnifiedCreationPlayId,
  mudPointCost: number,
): UnifiedCreationSpec {
  return {
    ...getUnifiedCreationSpec(playId),
    mudPointCost,
  };
}

const testCreationEntryConfig = {
  startCard: {
    title: '新建作品',
    description: '选择模板后进入对应的创作表单。',
    idleBadge: '模板 Tab',
    busyBadge: '正在开启',
  },
  typeModal: {
    title: '选择创作类型',
    description: '先选玩法类型，再进入对应创作工作台。',
  },
  eventBanner: {
    title: '泥点挑战',
    description: '创作活动测试横幅。',
    coverImageSrc: '/creation-type-references/puzzle.webp',
    prizePoolMudPoints: 1000,
    startsAtText: '2026-05-01',
    endsAtText: '2026-05-31',
  },
  eventBanners: [
    {
      title: '后台拼图赛',
      description: '后台配置的拼图横幅。',
      coverImageSrc: '/creation-type-references/puzzle.webp',
      prizePoolMudPoints: 1000,
      startsAtText: '2026-05-01',
      endsAtText: '2026-05-31',
      renderMode: 'structured' as const,
    },
    {
      title: '后台抓大鹅赛',
      description: '后台配置的抓大鹅横幅。',
      coverImageSrc: '/creation-type-references/match3d.webp',
      prizePoolMudPoints: 1200,
      startsAtText: '2026-06-01',
      endsAtText: '2026-06-30',
      renderMode: 'structured' as const,
    },
  ],
  creationTypes: [
    {
      id: 'rpg',
      title: '文字冒险',
      subtitle: '经典 RPG 体验',
      badge: '可创建',
      imageSrc: '/creation-type-references/rpg.webp',
      visible: true,
      open: true,
      sortOrder: 10,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
    },
    {
      id: 'puzzle',
      title: '拼图',
      subtitle: '拼图关卡创作',
      badge: '可创建',
      imageSrc: '/creation-type-references/puzzle.webp',
      visible: true,
      open: true,
      sortOrder: 30,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
      unifiedCreationSpec: buildTestUnifiedCreationSpec('puzzle', 8),
    },
    {
      id: 'match3d',
      title: '抓大鹅',
      subtitle: '3D 消除关卡',
      badge: '可创建',
      imageSrc: '/creation-type-references/match3d.webp',
      visible: true,
      open: true,
      sortOrder: 40,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
      unifiedCreationSpec: buildTestUnifiedCreationSpec('match3d', 12),
    },
    {
      id: 'bark-battle',
      title: '汪汪声浪',
      subtitle: '声控狗狗对战',
      badge: '可创建',
      imageSrc: '/creation-type-references/bark-battle.webp',
      visible: true,
      open: true,
      sortOrder: 45,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
      unifiedCreationSpec: buildTestUnifiedCreationSpec('bark-battle', 6),
    },
    {
      id: 'square-hole',
      title: '方洞挑战',
      subtitle: '形状投放挑战',
      badge: '可创建',
      imageSrc: '/creation-type-references/square-hole.webp',
      visible: false,
      open: true,
      sortOrder: 50,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
    },
    {
      id: 'visual-novel',
      title: '视觉小说',
      subtitle: '分支叙事体验',
      badge: '敬请期待',
      imageSrc: '/creation-type-references/visual-novel.webp',
      visible: false,
      open: false,
      sortOrder: 60,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
    },
    {
      id: 'airp',
      title: 'AIRP',
      subtitle: '敬请期待',
      badge: '即将开放',
      imageSrc: '/creation-type-references/airp.webp',
      visible: true,
      open: false,
      sortOrder: 70,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
    },
    {
      id: 'creative-agent',
      title: '智能创作',
      subtitle: '对话式创作实验',
      badge: '内测',
      imageSrc: '/creation-type-references/creative-agent.webp',
      visible: false,
      open: true,
      sortOrder: 80,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
    },
    {
      id: 'baby-object-match',
      title: '宝贝识物',
      subtitle: '亲子识物分类',
      badge: '可创建',
      imageSrc: '/child-motion-demo/picture-book-grass-stage.png',
      visible: true,
      open: true,
      sortOrder: 90,
      categoryId: 'recommended',
      categoryLabel: '热门推荐',
      categorySortOrder: 20,
      updatedAtMicros: 1,
    },
  ],
} satisfies CreationEntryConfig;

const rpgCreationServiceMocks = vi.hoisted(() => ({
  createRpgCreationSession: vi.fn(),
  deleteRpgCreationAgentSession: vi.fn(),
  executeRpgCreationAction: vi.fn(),
  getRpgCreationOperation: vi.fn(),
  getRpgCreationResultView: vi.fn(),
  getRpgCreationSession: vi.fn(),
  listRpgCreationWorks: vi.fn(),
  streamRpgCreationMessage: vi.fn(),
  upsertRpgWorldProfile: vi.fn(),
}));

const rpgEntryLibraryServiceMocks = vi.hoisted(() => ({
  deleteRpgEntryWorldProfile: vi.fn(),
  getRpgEntryWorldGalleryDetail: vi.fn(),
  getRpgEntryWorldGalleryDetailByCode: vi.fn(),
  getRpgEntryWorldLibraryDetail: vi.fn(),
  likeRpgEntryWorldGallery: vi.fn(),
  listRpgEntryWorldGallery: vi.fn(),
  listRpgEntryWorldLibrary: vi.fn(),
  publishRpgEntryWorldProfile: vi.fn(),
  recordRpgEntryWorldGalleryPlay: vi.fn(),
  remixRpgEntryWorldGallery: vi.fn(),
  unpublishRpgEntryWorldProfile: vi.fn(),
  upsertRpgEntryWorldProfile: vi.fn(),
}));

vi.mock('../../services/rpg-creation', () => ({
  ...rpgCreationServiceMocks,
}));

vi.mock('../../services/rpg-creation/index', () => ({
  ...rpgCreationServiceMocks,
}));

vi.mock('../../services/rpg-entry', () => ({
  clearRpgProfileBrowseHistory: vi.fn(),
  deleteRpgEntryWorldProfile:
    rpgEntryLibraryServiceMocks.deleteRpgEntryWorldProfile,
  getRpgEntryWorldGalleryDetail:
    rpgEntryLibraryServiceMocks.getRpgEntryWorldGalleryDetail,
  getRpgProfileDashboard: vi.fn(),
  listRpgEntryWorldGallery:
    rpgEntryLibraryServiceMocks.listRpgEntryWorldGallery,
  listRpgEntryWorldLibrary:
    rpgEntryLibraryServiceMocks.listRpgEntryWorldLibrary,
  listRpgProfileBrowseHistory: vi.fn(),
  listRpgProfileSaveArchives: vi.fn(),
  publishRpgEntryWorldProfile:
    rpgEntryLibraryServiceMocks.publishRpgEntryWorldProfile,
  recordRpgEntryWorldGalleryPlay:
    rpgEntryLibraryServiceMocks.recordRpgEntryWorldGalleryPlay,
  resumeRpgProfileSaveArchive: vi.fn(),
  remixRpgEntryWorldGallery:
    rpgEntryLibraryServiceMocks.remixRpgEntryWorldGallery,
  syncRpgProfileBrowseHistory: vi.fn(),
  unpublishRpgEntryWorldProfile:
    rpgEntryLibraryServiceMocks.unpublishRpgEntryWorldProfile,
  upsertRpgProfileBrowseHistory: vi.fn(),
}));

vi.mock('../../services/creationEntryConfigService', () => ({
  DEFAULT_UNIFIED_CREATION_MUD_POINT_COST: 10,
  fetchCreationEntryConfig: vi.fn(),
}));

vi.mock('../../services/puzzle-works', () => ({
  listPuzzleWorks: vi.fn(),
  updatePuzzleWork: vi.fn(),
}));

vi.mock('../../services/puzzle-gallery', () => ({
  getPuzzleGalleryDetail: vi.fn(),
  listPuzzleGallery: vi.fn(),
  remixPuzzleGalleryWork: vi.fn(),
}));

vi.mock('../../services/puzzle-runtime', () => ({
  advancePuzzleNextLevel: vi.fn(),
  dragPuzzlePieceOrGroup: vi.fn(),
  getPuzzleRun: vi.fn(),
  startPuzzleRun: vi.fn(),
  swapPuzzlePieces: vi.fn(),
  submitPuzzleLeaderboard: vi.fn(),
  updatePuzzleRunPause: vi.fn(),
  usePuzzleRuntimeProp: vi.fn(),
}));

vi.mock('../../services/jump-hop/jumpHopClient', () => ({
  jumpHopClient: {
    createSession: vi.fn(),
    deleteWork: vi.fn(),
    executeAction: vi.fn(),
    getGalleryDetail: vi.fn(),
    getLeaderboard: vi.fn(),
    getSession: vi.fn(),
    getWorkDetail: vi.fn(),
    listGallery: vi.fn(),
    listWorks: vi.fn(),
    publishWork: vi.fn(),
    restartRun: vi.fn(),
    startRun: vi.fn(),
    submitJump: vi.fn(),
  },
}));

vi.mock('../../services/rpg-entry/rpgEntryLibraryClient', () => ({
  ...rpgEntryLibraryServiceMocks,
}));

vi.mock('../../services/big-fish-creation', () => ({
  createBigFishCreationSession: vi.fn(),
  executeBigFishCreationAction: vi.fn(),
  getBigFishCreationSession: vi.fn(),
  streamBigFishCreationMessage: vi.fn(),
}));

vi.mock('../../services/big-fish-works', () => ({
  listBigFishWorks: vi.fn(),
}));

vi.mock('../../services/big-fish-gallery', () => ({
  listBigFishGallery: vi.fn(),
}));

vi.mock('../../services/big-fish-runtime', () => ({
  recordBigFishPlay: vi.fn().mockResolvedValue({ items: [] }),
  startBigFishRun: vi.fn(),
  submitBigFishInput: vi.fn(),
}));

vi.mock('../../services/bark-battle-creation', () => ({
  createBarkBattleDraft: vi.fn(),
  deleteBarkBattleWork: vi.fn(),
  generateAllBarkBattleImageAssets: vi.fn(),
  listBarkBattleGallery: vi.fn(),
  listBarkBattleWorks: vi.fn(),
  publishBarkBattleWork: vi.fn(),
  regenerateBarkBattleImageAsset: vi.fn(),
  updateBarkBattleDraftConfig: vi.fn(),
  uploadBarkBattleAsset: vi.fn(),
}));

vi.mock('../../services/edutainment-baby-object', () => ({
  createBabyObjectMatchDraft: vi.fn(),
  deleteLocalBabyObjectMatchDraft: vi.fn(),
  hasBabyObjectMatchPlaceholderAssets: vi.fn(() => false),
  listLocalBabyObjectMatchDrafts: vi.fn(),
  publishBabyObjectMatchWork: vi.fn(),
  regenerateBabyObjectMatchDraftAssets: vi.fn(),
  saveBabyObjectMatchDraft: vi.fn(),
}));

vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
  woodenFishClient: {
    checkpointRun: vi.fn(),
    createSession: vi.fn(),
    deleteWork: vi.fn(),
    executeAction: vi.fn(),
    finishRun: vi.fn(),
    getGalleryDetail: vi.fn(),
    getSession: vi.fn(),
    getWorkDetail: vi.fn(),
    listGallery: vi.fn(async () => ({
      hasMore: false,
      items: [],
      nextCursor: null,
    })),
    listWorks: vi.fn(async () => ({ items: [] })),
    publishWork: vi.fn(),
    restartRun: vi.fn(),
    startRun: vi.fn(),
  },
}));

vi.mock('../../services/match3d-creation', () => ({
  match3dCreationClient: {
    createSession: vi.fn(),
    executeAction: vi.fn(),
    getSession: vi.fn(),
    streamMessage: vi.fn(),
  },
}));

vi.mock('../../services/match3d-works', () => ({
  deleteMatch3DWork: vi.fn(),
  getMatch3DWorkDetail: vi.fn(),
  listMatch3DGallery: vi.fn(),
  listMatch3DWorks: vi.fn(),
  updateMatch3DGeneratedItemAssets: vi.fn(),
}));

vi.mock('../../services/match3dGeneratedModelCache', () => ({
  hasMatch3DGeneratedImageAsset: vi.fn(
    (assets: Match3DWorkSummary['generatedItemAssets']) =>
      Boolean(
        assets?.some(
          (asset) =>
            asset.imageSrc?.trim() ||
            asset.imageObjectKey?.trim() ||
            asset.imageViews?.some(
              (view) => view.imageSrc?.trim() || view.imageObjectKey?.trim(),
            ),
        ),
      ),
  ),
  normalizeMatch3DGeneratedItemAssetsForRuntime: vi.fn(
    (assets: Match3DWorkSummary['generatedItemAssets']) =>
      assets ? [...assets] : [],
  ),
  mergeMatch3DGeneratedItemAssetsForRuntime: vi.fn(
    (
      primaryAssets: Match3DWorkSummary['generatedItemAssets'],
      fallbackAssets: Match3DWorkSummary['generatedItemAssets'],
    ) =>
      primaryAssets
        ? [...primaryAssets]
        : fallbackAssets
          ? [...fallbackAssets]
          : [],
  ),
  preloadMatch3DGeneratedRuntimeAssets: vi.fn(() => Promise.resolve()),
}));

const match3dRuntimeServiceMocks = vi.hoisted(() => ({
  createServerMatch3DRuntimeAdapter: vi.fn(),
}));

const match3dServerRuntimeAdapterMock = vi.hoisted(() => ({
  clickItem: vi.fn(),
  finishTimeUp: vi.fn(),
  getRun: vi.fn(),
  restartRun: vi.fn(),
  startRun: vi.fn(),
  stopRun: vi.fn(),
}));

vi.mock('../../services/match3d-runtime', async () => {
  const actual = await vi.importActual<
    typeof import('../../services/match3d-runtime')
  >('../../services/match3d-runtime');

  return {
    ...actual,
    ...match3dRuntimeServiceMocks,
  };
});

vi.mock('../../services/square-hole-creation', () => ({
  squareHoleCreationClient: {
    createSession: vi.fn(),
    executeAction: vi.fn(),
    getSession: vi.fn(),
    sendMessage: vi.fn(),
    streamMessage: vi.fn(),
  },
}));

vi.mock('../../services/square-hole-runtime', () => ({
  dropSquareHoleShape: vi.fn(),
  finishSquareHoleTimeUp: vi.fn(),
  getSquareHoleRun: vi.fn(),
  restartSquareHoleRun: vi.fn(),
  startSquareHoleRun: vi.fn(),
  stopSquareHoleRun: vi.fn(),
}));

vi.mock('../../services/square-hole-works', () => ({
  deleteSquareHoleWork: vi.fn(),
  getSquareHoleWorkDetail: vi.fn(),
  listSquareHoleGallery: vi.fn(),
  listSquareHoleWorks: vi.fn(),
}));

vi.mock('../../services/visual-novel-runtime', () => ({
  listVisualNovelGallery: vi.fn(),
  startVisualNovelRun: vi.fn(),
  streamVisualNovelRuntimeAction: vi.fn(),
}));

vi.mock('../../services/visual-novel-works', () => ({
  deleteVisualNovelWork: vi.fn(),
  getVisualNovelWorkDetail: vi.fn(),
  listVisualNovelWorks: vi.fn(),
  publishVisualNovelWork: vi.fn(),
  updateVisualNovelWork: vi.fn(),
}));

vi.mock('../../services/visual-novel-creation', () => ({
  compileVisualNovelWorkProfile: vi.fn(),
  createVisualNovelSession: vi.fn(),
  executeVisualNovelAction: vi.fn(),
  getVisualNovelSession: vi.fn(),
  streamVisualNovelMessage: vi.fn(),
}));

vi.mock('../../services/creative-agent', () => ({
  cancelCreativeAgentSession: vi.fn(),
  confirmCreativePuzzleTemplate: vi.fn(),
  createCreativeAgentSession: vi.fn(),
  streamCreativeAgentMessage: vi.fn(),
  streamCreativeDraftEdit: vi.fn(),
}));

vi.mock('../../services/puzzle-runtime/puzzleLocalRuntime', async () => {
  const actual = await vi.importActual<
    typeof import('../../services/puzzle-runtime/puzzleLocalRuntime')
  >('../../services/puzzle-runtime/puzzleLocalRuntime');

  return {
    ...actual,
    dragLocalPuzzlePiece: vi.fn(actual.dragLocalPuzzlePiece),
    startLocalPuzzleRun: vi.fn(
      (...args: Parameters<typeof actual.startLocalPuzzleRun>) =>
        actual.startLocalPuzzleRun(...args),
    ),
    swapLocalPuzzlePieces: vi.fn(actual.swapLocalPuzzlePieces),
  };
});

vi.mock('../../services/puzzle-onboarding', () => ({
  generatePuzzleOnboardingWork: vi.fn(),
  savePuzzleOnboardingWork: vi.fn(),
}));

vi.mock('../../services/puzzle-agent', () => ({
  createPuzzleAgentSession: vi.fn(),
  executePuzzleAgentAction: vi.fn(),
  getPuzzleAgentSession: vi.fn(),
  streamPuzzleAgentMessage: vi.fn(),
}));

vi.mock('../unified-creation/workspaces/PuzzleCreationWorkspace', () => ({
  PuzzleCreationWorkspace: ({
    session,
    isBusy,
    error,
    onBack,
    onExecuteAction,
    onCreateFromForm,
  }: {
    session: { sessionId: string; messages: Array<{ text: string }> } | null;
    isBusy?: boolean;
    error?: string | null;
    onBack: () => void;
    onExecuteAction: (payload: PuzzleAgentActionRequest) => void;
    onCreateFromForm?: (payload: CreatePuzzleAgentSessionRequest) => void;
  }) => (
    <div className="puzzle-agent-workspace-mock">
      <div>拼图工作区：{session?.sessionId ?? 'missing-session'}</div>
      <div data-testid="puzzle-workspace-busy-state">
        {isBusy ? 'busy' : 'idle'}
      </div>
      {session?.messages.map((message) => (
        <div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
      ))}
      {error ? <div>{error}</div> : null}
      <button
        type="button"
        disabled={isBusy}
        onClick={() => {
          const payload = {
            seedText: '暖灯猫街',
            workTitle: '暖灯猫街',
            workDescription: '一套雨夜猫街主题拼图。',
            pictureDescription: '一只猫在雨夜灯牌下回头。',
            referenceImageSrc: null,
          };
          if (session) {
            onExecuteAction({
              action: 'compile_puzzle_draft',
              promptText: payload.pictureDescription,
              ...payload,
              candidateCount: 1,
            });
            return;
          }
          onCreateFromForm?.(payload);
        }}
      >
        生成草稿
      </button>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../puzzle-result/PuzzleResultView', () => ({
  PuzzleResultView: ({
    isBusy,
    onExecuteAction,
    onStartTestRun,
    session,
    onBack,
  }: {
    isBusy?: boolean;
    onExecuteAction: (payload: {
      action: string;
      levelId?: string;
      promptText?: string;
    }) => void;
    onStartTestRun?: (draft: PuzzleResultDraft) => void;
    session: { draft?: PuzzleResultDraft | null };
    onBack: () => void;
  }) => (
    <div className="puzzle-result-view-mock">
      <div>拼图结果页</div>
      <label>
        关卡名
        <input readOnly value={session.draft?.levelName ?? ''} />
      </label>
      <button
        type="button"
        onClick={() => {
          onExecuteAction({
            action: 'generate_puzzle_images',
            levelId: 'puzzle-level-1',
            promptText: '重新生成猫街',
          });
        }}
      >
        重新生成画面
      </button>
      <button
        type="button"
        disabled={!session.draft}
        onClick={() => {
          if (session.draft) {
            onStartTestRun?.(session.draft);
          }
        }}
      >
        试玩
      </button>
      <button type="button" disabled={isBusy}>
        新增关卡
      </button>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../puzzle-gallery/PuzzleGalleryDetailView', () => ({
  PuzzleGalleryDetailView: ({
    item,
    onBack,
    onStartGame,
  }: {
    item: { levelName: string };
    onBack: () => void;
    onStartGame: () => void;
  }) => (
    <div className="puzzle-gallery-detail-view-mock">
      <div>{item.levelName}</div>
      <button type="button" onClick={onStartGame}>
        进入第 1 关
      </button>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../big-fish-creation/BigFishAgentWorkspace', () => ({
  BigFishAgentWorkspace: ({
    session,
  }: {
    session: { sessionId: string; messages: Array<{ text: string }> } | null;
  }) => (
    <div className="big-fish-agent-workspace-mock">
      <div>大鱼吃小鱼工作区：{session?.sessionId ?? 'missing-session'}</div>
      {session?.messages.map((message) => (
        <div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
      ))}
    </div>
  ),
}));

vi.mock('../big-fish-result/BigFishResultView', () => ({
  BigFishResultView: ({
    session,
    onBack,
    onExecuteAction,
  }: {
    session: { draft?: { title: string } | null };
    onBack: () => void;
    onExecuteAction: (payload: { action: string }) => void;
  }) => (
    <div className="big-fish-result-view-mock">
      <div>大鱼吃小鱼结果页</div>
      <div>{session.draft?.title ?? '缺少草稿标题'}</div>
      <button
        type="button"
        onClick={() => {
          onExecuteAction({ action: 'big_fish_publish_game' });
        }}
      >
        发布
      </button>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../match3d-result/Match3DResultView', () => ({
  Match3DResultView: ({
    draft,
    onBack,
    onStartTestRun,
    profile,
  }: {
    draft?: { gameName?: string | null } | null;
    onBack: () => void;
    onStartTestRun: (profile: Match3DWorkSummary) => void;
    profile: Match3DWorkSummary;
  }) => (
    <div className="match3d-result-view-mock">
      <div>抓大鹅结果页</div>
      <div>{draft?.gameName ?? profile.gameName}</div>
      <button type="button" onClick={() => onStartTestRun(profile)}>
        试玩
      </button>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../unified-creation/workspaces/Match3DCreationWorkspace', () => ({
  Match3DCreationWorkspace: ({
    session,
    isBusy,
    error,
    onCreateFromForm,
  }: {
    session: { sessionId: string; messages: Array<{ text: string }> } | null;
    isBusy?: boolean;
    error?: string | null;
    onCreateFromForm?: (payload: {
      seedText: string;
      themeText: string;
      referenceImageSrc: string | null;
      clearCount: number;
      difficulty: number;
      generateClickSound?: boolean;
    }) => void;
  }) => (
    <div className="match3d-agent-workspace-mock">
      <div>抓大鹅工作区：{session?.sessionId ?? 'missing-session'}</div>
      {session?.messages.map((message) => (
        <div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
      ))}
      <div data-testid="match3d-workspace-busy-state">
        {isBusy ? 'busy' : 'idle'}
      </div>
      {error ? <div>{error}</div> : null}
      <button
        type="button"
        disabled={isBusy}
        onClick={() => {
          onCreateFromForm?.({
            seedText: '赛博水果摊题材，消除9次，难度6',
            themeText: '赛博水果摊',
            referenceImageSrc: null,
            clearCount: 9,
            difficulty: 6,
          });
        }}
      >
        生成抓大鹅草稿
      </button>
    </div>
  ),
}));

vi.mock('../match3d-runtime/Match3DRuntimeShell', () => ({
  Match3DRuntimeShell: ({
    run,
    generatedItemAssets = [],
    generatedBackgroundAsset = null,
    onBack,
  }: {
    run: Match3DRunSnapshot | null;
    generatedItemAssets?: Match3DWorkSummary['generatedItemAssets'];
    generatedBackgroundAsset?: Match3DWorkSummary['generatedBackgroundAsset'];
    onBack: () => void;
  }) => (
    <div className="match3d-runtime-shell-mock">
      <div>抓大鹅运行态：{run?.runId ?? 'missing-run'}</div>
      <div data-testid="match3d-runtime-generated-model-count">
        {
          generatedItemAssets.filter(
            (asset) => asset.modelSrc?.trim() || asset.modelObjectKey?.trim(),
          ).length
        }
      </div>
      <div data-testid="match3d-runtime-generated-asset-count">
        {
          generatedItemAssets.filter(
            (asset) =>
              asset.modelSrc?.trim() ||
              asset.modelObjectKey?.trim() ||
              asset.imageSrc?.trim() ||
              asset.imageObjectKey?.trim() ||
              asset.imageViews?.some(
                (view) => view.imageSrc?.trim() || view.imageObjectKey?.trim(),
              ) ||
              asset.backgroundMusic?.audioSrc?.trim() ||
              asset.clickSound?.audioSrc?.trim() ||
              asset.backgroundAsset?.imageSrc?.trim() ||
              asset.backgroundAsset?.imageObjectKey?.trim() ||
              asset.backgroundAsset?.containerImageSrc?.trim() ||
              asset.backgroundAsset?.containerImageObjectKey?.trim(),
          ).length
        }
      </div>
      <div data-testid="match3d-runtime-generated-item-image-count">
        {
          generatedItemAssets.filter(
            (asset) =>
              asset.imageSrc?.trim() ||
              asset.imageObjectKey?.trim() ||
              asset.imageViews?.some(
                (view) => view.imageSrc?.trim() || view.imageObjectKey?.trim(),
              ),
          ).length
        }
      </div>
      <div data-testid="match3d-runtime-background-music-count">
        {
          generatedItemAssets.filter((asset) =>
            asset.backgroundMusic?.audioSrc?.trim(),
          ).length
        }
      </div>
      <div data-testid="match3d-runtime-container-ui-count">
        {
          generatedItemAssets.filter(
            (asset) =>
              asset.backgroundAsset?.containerImageSrc?.trim() ||
              asset.backgroundAsset?.containerImageObjectKey?.trim(),
          ).length
        }
      </div>
      <div data-testid="match3d-runtime-top-level-background-count">
        {generatedBackgroundAsset?.imageSrc?.trim() ||
        generatedBackgroundAsset?.imageObjectKey?.trim()
          ? 1
          : 0}
      </div>
      <div data-testid="match3d-runtime-top-level-container-ui-count">
        {generatedBackgroundAsset?.containerImageSrc?.trim() ||
        generatedBackgroundAsset?.containerImageObjectKey?.trim()
          ? 1
          : 0}
      </div>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
  BarkBattleConfigEditor: ({
    error,
    isBusy,
    showBackButton,
    title,
    onPreview,
  }: {
    error?: string | null;
    isBusy?: boolean;
    showBackButton?: boolean;
    title?: string | null;
    onPreview: (payload: {
      title: string;
      description: string;
      themeDescription: string;
      playerImageDescription: string;
      opponentImageDescription: string;
      difficultyPreset: 'normal';
    }) => void;
  }) => (
    <div className="bark-battle-config-editor-mock">
      <div>汪汪声浪配置表单</div>
      <label>
        作品标题
        <input aria-label="汪汪作品标题" defaultValue="汪汪测试杯" />
      </label>
      <div data-testid="bark-battle-editor-back-state">
        {showBackButton ? 'back-visible' : 'back-hidden'}
      </div>
      <div data-testid="bark-battle-editor-title-state">
        {title === null ? 'title-hidden' : title}
      </div>
      <div data-testid="bark-battle-editor-busy-state">
        {isBusy ? 'busy' : 'idle'}
      </div>
      {error ? <div>{error}</div> : null}
      <button
        type="button"
        disabled={isBusy}
        onClick={() => {
          onPreview({
            title: '汪汪测试杯',
            description: '',
            themeDescription: '阳光草坪声浪竞技场',
            playerImageDescription: '戴红色围巾的柯基选手',
            opponentImageDescription: '蓝色护目镜哈士奇对手',
            difficultyPreset: 'normal',
          });
        }}
      >
        生成草稿
      </button>
    </div>
  ),
}));

vi.mock('../bark-battle-creation/BarkBattleResultView', () => ({
  BarkBattleResultView: ({
    draft,
    onBack,
    onPublish,
    onStartTestRun,
  }: {
    draft: {
      title: string;
      draftId: string;
      workId?: string;
    };
    onBack: () => void;
    onPublish: (draft: unknown) => void;
    onStartTestRun: (draft: unknown) => void;
  }) => (
    <div className="bark-battle-result-view-mock">
      <div>汪汪声浪结果页：{draft.title}</div>
      <div>草稿ID：{draft.draftId}</div>
      <div>作品ID：{draft.workId ?? 'missing-work'}</div>
      <button type="button" onClick={() => onStartTestRun(draft)}>
        试玩
      </button>
      <button type="button" onClick={() => onPublish(draft)}>
        发布
      </button>
      <button type="button" onClick={onBack}>
        返回编辑
      </button>
    </div>
  ),
}));

vi.mock('../edutainment-result/BabyObjectMatchResultView', () => ({
  BabyObjectMatchResultView: ({
    draft,
    onBack,
    onStartTestRun,
  }: {
    draft: BabyObjectMatchDraft;
    onBack: () => void;
    onStartTestRun?: (draft: BabyObjectMatchDraft) => void;
  }) => (
    <div className="baby-object-match-result-view-mock">
      <div>宝贝识物结果页</div>
      <div>{draft.workTitle}</div>
      <button
        type="button"
        onClick={() => {
          onStartTestRun?.(draft);
        }}
      >
        试玩
      </button>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../edutainment-runtime/BabyObjectMatchRuntimeShell', () => ({
  BabyObjectMatchRuntimeShell: ({
    draft,
    onBack,
  }: {
    draft: BabyObjectMatchDraft;
    onBack?: () => void;
  }) => (
    <div className="baby-object-match-runtime-shell-mock">
      <div>宝贝识物运行态：{draft.profileId}</div>
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../../games/bark-battle/ui/BarkBattleRuntimeShell', () => ({
  BarkBattleRuntimeShell: ({
    title,
    workId,
    runtimeMode,
    publishedConfig,
    onExit,
  }: {
    title?: string;
    workId?: string;
    runtimeMode?: string;
    publishedConfig?: {
      workId?: string;
      playerCharacterImageSrc?: string | null;
    } | null;
    onExit?: () => void;
  }) => (
    <div className="bark-battle-runtime-shell-mock">
      <div>
        汪汪声浪运行态：{title ?? '未命名'} / {workId ?? 'missing-work'}
      </div>
      <div data-testid="bark-battle-runtime-mode">
        {runtimeMode ?? 'missing-mode'}
      </div>
      <div data-testid="bark-battle-runtime-work-id">
        {publishedConfig?.workId ?? 'missing-config-work'}
      </div>
      <div data-testid="bark-battle-runtime-player-src">
        {publishedConfig?.playerCharacterImageSrc ?? 'missing-player-src'}
      </div>
      <button type="button" onClick={onExit}>
        返回配置
      </button>
    </div>
  ),
}));

vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
  CustomWorldAgentWorkspace: ({
    session,
    onExecuteAction,
  }: {
    session: CustomWorldAgentSessionSnapshot | null;
    onExecuteAction: (payload: { action: string }) => void;
  }) => (
    <div className="agent-workspace-mock">
      Agent工作区：{session?.sessionId ?? 'missing-session'}
      <button
        type="button"
        onClick={() => {
          onExecuteAction({
            action: 'draft_foundation',
          });
        }}
      >
        开始生成草稿
      </button>
    </div>
  ),
}));

const mockSession: CustomWorldAgentSessionSnapshot = {
  sessionId: 'custom-world-agent-session-1',
  currentTurn: 0,
  anchorContent: {
    worldPromise:
      '被海雾吞没的旧航路群岛，灯塔与禁航令共同决定谁能穿过死潮，体验压抑、潮湿、悬疑。',
    playerFantasy:
      '玩家是被迫返乡的守灯人继承者，追查沉船夜与假航灯的关系，风险是失去家族最后一条可信航线。',
    themeBoundary: '压抑、悬疑；潮湿群岛、冷雾港口；避免轻喜冒险。',
    playerEntryPoint:
      '玩家以返乡守灯人继承者身份切入，回港首夜撞见禁航区假航灯重亮，动机是阻止更多船只误入死潮。',
    coreConflict:
      '守灯会与航运公会争夺航路解释权，有人在借假航灯持续清洗旧案证据，玩家返乡当夜就被卷进封航冲突。',
    keyRelationships: '玩家与沈砺旧友互疑，沈砺知道沉船夜的另一半真相。',
    hiddenLines:
      '沉船夜与假航灯骗局属于同一操盘链条，表面像海雾自然失控，揭示节奏是先见异常，再见旧案，再见操盘者。',
    iconicElements:
      '假航灯、沉钟回响、旧灯塔、禁航碑；错误航灯会把船引进必死水域。',
  },
  progressPercent: 0,
  lastAssistantReply: '先告诉我你想做一个怎样的 RPG 世界。',
  stage: 'clarifying',
  focusCardId: null,
  creatorIntent: {},
  creatorIntentReadiness: {
    isReady: false,
    completedKeys: ['world_hook'],
    missingKeys: [
      'player_premise',
      'theme_and_tone',
      'core_conflict',
      'relationship_seed',
      'iconic_element',
    ],
  },
  anchorPack: {},
  lockState: {},
  draftProfile: null,
  messages: [
    {
      id: 'message-1',
      role: 'assistant',
      kind: 'summary',
      text: '先告诉我你想做一个怎样的 RPG 世界。',
      createdAt: '2026-04-14T12:00:00.000Z',
      relatedOperationId: null,
    },
  ],
  draftCards: [],
  pendingClarifications: [],
  suggestedActions: [],
  recommendedReplies: [],
  qualityFindings: [],
  assetCoverage: {
    roleAssets: [],
    sceneAssets: [],
    allRoleAssetsReady: false,
    allSceneAssetsReady: false,
  },
  updatedAt: '2026-04-14T12:00:00.000Z',
};

const mockAuthUser: AuthUser = {
  id: 'user-1',
  displayName: '测试玩家',
  avatarUrl: null,
  publicUserCode: 'user-tester',
  phoneNumberMasked: null,
  loginMethod: 'password',
  bindingStatus: 'active',
  wechatBound: false,
};

function buildMockCreativeAgentSession(
  overrides: Partial<CreativeAgentSessionSnapshot> = {},
): CreativeAgentSessionSnapshot {
  const sessionId = overrides.sessionId ?? 'creative-agent-session-1';

  return {
    sessionId,
    stage: 'waiting_user',
    inputSummary: {
      text: null,
      entryContext: 'creation_home',
      images: [],
      materialSummary: null,
      unsupportedCapabilities: [],
    },
    messages: [
      {
        id: 'creative-agent-message-1',
        role: 'assistant',
        kind: 'chat',
        text: '说一个灵感，我来帮你做成互动内容。',
        createdAt: '2026-05-05T10:00:00.000Z',
      },
    ],
    puzzleTemplateCatalog: [],
    puzzleTemplateSelection: null,
    puzzleImageGenerationPlan: null,
    targetBinding: null,
    updatedAt: '2026-05-05T10:00:00.000Z',
    ...overrides,
  };
}

function buildMockBabyObjectMatchDraft(
  overrides: Partial<BabyObjectMatchDraft> = {},
): BabyObjectMatchDraft {
  const itemNames = overrides.itemNames ?? ['苹果', '香蕉'];
  const now = '2026-05-14T10:00:00.000Z';

  return {
    draftId: 'baby-object-draft-red-dot',
    profileId: 'baby-object-profile-red-dot',
    templateId: 'baby-object-match',
    templateName: '宝贝识物',
    workTitle: '宝贝识物红点草稿',
    workDescription: `${itemNames[0]}和${itemNames[1]}识物分类`,
    itemNames,
    itemAssets: [
      {
        itemId: 'baby-object-item-a',
        itemName: itemNames[0],
        imageSrc: '/baby-object/apple.png',
        assetObjectId: null,
        generationProvider: 'vector-engine-gpt-image-2',
        prompt: itemNames[0],
      },
      {
        itemId: 'baby-object-item-b',
        itemName: itemNames[1],
        imageSrc: '/baby-object/banana.png',
        assetObjectId: null,
        generationProvider: 'vector-engine-gpt-image-2',
        prompt: itemNames[1],
      },
    ],
    visualPackage: null,
    themeTags: ['寓教于乐', '宝贝识物'],
    publicationStatus: 'draft',
    createdAt: now,
    updatedAt: now,
    publishedAt: null,
    ...overrides,
  };
}

function buildMockJumpHopWork(
  overrides: Partial<JumpHopWorkProfileResponse> = {},
): JumpHopWorkProfileResponse {
  const profileId = overrides.summary?.profileId ?? 'jump-hop-profile-1';
  const path = overrides.path ?? {
    seed: 'jump-hop-seed',
    difficulty: 'standard' as const,
    platforms: [
      {
        platformId: 'platform-start',
        tileType: 'start' as const,
        x: 0,
        y: 0,
        width: 48,
        height: 36,
        landingRadius: 22,
        perfectRadius: 12,
        scoreValue: 1,
      },
      {
        platformId: 'platform-finish',
        tileType: 'finish' as const,
        x: 16,
        y: 18,
        width: 60,
        height: 42,
        landingRadius: 22,
        perfectRadius: 12,
        scoreValue: 2,
      },
    ],
    finishIndex: 1,
    cameraPreset: 'default',
    scoring: {
      chargeToDistanceRatio: 1.2,
      maxChargeMs: 1800,
      hitBonus: 20,
      perfectBonus: 50,
    },
  };
  const characterAsset = overrides.characterAsset ?? {
    assetId: 'jump-hop-character-1',
    imageSrc: 'data:image/png;base64,character',
    imageObjectKey: 'jump-hop/character.png',
    assetObjectId: 'asset-jump-hop-character',
    generationProvider: 'vector-engine-gpt-image-2',
    prompt: '纸片小兔',
    width: 1024,
    height: 1024,
  };
  const tileAtlasAsset = overrides.tileAtlasAsset ?? {
    assetId: 'jump-hop-tiles-1',
    imageSrc: 'data:image/png;base64,tiles',
    imageObjectKey: 'jump-hop/tiles.png',
    assetObjectId: 'asset-jump-hop-tiles',
    generationProvider: 'vector-engine-gpt-image-2',
    prompt: '柔软云朵平台',
    width: 1024,
    height: 1024,
  };
  const tileAssets = overrides.tileAssets ?? [
    {
      tileType: 'start' as const,
      imageSrc: 'data:image/png;base64,tile-start',
      imageObjectKey: 'jump-hop/tile-start.png',
      assetObjectId: 'asset-jump-hop-tile-start',
      sourceAtlasCell: 'A1',
      visualWidth: 128,
      visualHeight: 96,
      topSurfaceRadius: 24,
      landingRadius: 28,
    },
    {
      tileType: 'finish' as const,
      imageSrc: 'data:image/png;base64,tile-finish',
      imageObjectKey: 'jump-hop/tile-finish.png',
      assetObjectId: 'asset-jump-hop-tile-finish',
      sourceAtlasCell: 'A2',
      visualWidth: 128,
      visualHeight: 96,
      topSurfaceRadius: 24,
      landingRadius: 28,
    },
  ];
  const draft = overrides.draft ?? {
    templateId: 'jump-hop',
    templateName: '跳一跳',
    profileId,
    themeText: '云朵跳台',
    workTitle: '云端跳台',
    workDescription: '一路跳到星星。',
    themeTags: ['云朵', '星空'],
    difficulty: 'standard' as const,
    stylePreset: 'paper-toy' as const,
    characterPrompt: '纸片小兔',
    tilePrompt: '柔软云朵平台',
    endMoodPrompt: '星光门',
    characterAsset,
    tileAtlasAsset,
    tileAssets,
    path,
    coverComposite: 'data:image/png;base64,cover',
    backButtonAsset: null,
    generationStatus: 'ready' as const,
  };

  return {
    summary: {
      runtimeKind: 'jump-hop',
      workId: 'jump-hop-work-1',
      profileId,
      ownerUserId: 'user-1',
      sourceSessionId: 'jump-hop-session-1',
      themeText: draft.themeText,
      workTitle: draft.workTitle,
      workDescription: draft.workDescription,
      themeTags: draft.themeTags,
      difficulty: draft.difficulty,
      stylePreset: draft.stylePreset,
      coverImageSrc: draft.coverComposite,
      publicationStatus: 'draft',
      playCount: 0,
      updatedAt: '2026-05-30T10:00:00.000Z',
      publishedAt: null,
      publishReady: true,
      generationStatus: 'ready',
      ...overrides.summary,
    },
    draft,
    path,
    characterAsset,
    tileAtlasAsset,
    tileAssets,
    backButtonAsset: null,
  };
}

function buildMockJumpHopRuntimeRun(
  work: JumpHopWorkProfileResponse,
  overrides: Partial<JumpHopRuntimeRunSnapshotResponse> = {},
): JumpHopRuntimeRunSnapshotResponse {
  return {
    runId: 'jump-hop-run-1',
    profileId: work.summary.profileId,
    ownerUserId: work.summary.ownerUserId,
    status: 'playing',
    currentPlatformIndex: 0,
    successfulJumpCount: 0,
    durationMs: 0,
    score: 0,
    combo: 0,
    path: work.path,
    lastJump: null,
    startedAtMs: 1_779_999_000_000,
    finishedAtMs: null,
    ...overrides,
  };
}

function buildMockBarkBattleWork(
  overrides: Partial<BarkBattleWorkSummary> = {},
): BarkBattleWorkSummary {
  return {
    workId: 'BB-C661A45F',
    draftId: 'bark-battle-draft-public-1',
    ownerUserId: 'user-1',
    authorDisplayName: '测试玩家',
    title: '汪汪公开杯',
    summary: '',
    themeDescription: '霓虹城市公园里的声浪擂台',
    playerImageDescription: '戴红围巾的柴犬主角',
    opponentImageDescription: '戴蓝色头带的哈士奇对手',
    playerCharacterImageSrc: '/generated-bark-battle/player.png',
    opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
    uiBackgroundImageSrc: '/generated-bark-battle/background.png',
    difficultyPreset: 'normal',
    status: 'published',
    generationStatus: 'ready',
    publishReady: true,
    playCount: 0,
    finishCount: 0,
    updatedAt: '2026-05-14T10:00:00.000Z',
    publishedAt: '2026-05-14T10:00:00.000Z',
    ...overrides,
  };
}

function buildMockSquareHoleAgentSession(
  overrides: Partial<
    Parameters<typeof buildMockSquareHoleAgentSessionImpl>[0]
  > = {},
) {
  return buildMockSquareHoleAgentSessionImpl(overrides);
}

function buildMockSquareHoleAgentSessionImpl(
  overrides: Partial<{
    sessionId: string;
    stage: string;
    messages: Array<{
      id: string;
      role: string;
      kind: string;
      text: string;
      createdAt: string;
    }>;
    updatedAt: string;
  }> = {},
) {
  const sessionId = overrides.sessionId ?? 'square-hole-session-1';

  return {
    sessionId,
    currentTurn: 0,
    progressPercent: 20,
    stage: 'collecting_config',
    anchorPack: {
      theme: {
        key: 'theme',
        label: '题材主题',
        value: '霓虹形状',
        status: 'confirmed',
      },
      twistRule: {
        key: 'twistRule',
        label: '反直觉规则',
        value: '颜色会误导洞口',
        status: 'confirmed',
      },
      shapeCount: {
        key: 'shapeCount',
        label: '形状数量',
        value: '12',
        status: 'confirmed',
      },
      difficulty: {
        key: 'difficulty',
        label: '难度',
        value: '5',
        status: 'confirmed',
      },
    },
    config: {
      themeText: '霓虹形状',
      twistRule: '颜色会误导洞口',
      shapeCount: 12,
      difficulty: 5,
      shapeOptions: [
        {
          optionId: 'shape-square',
          shapeKind: 'square',
          label: '方块',
          targetHoleId: 'hole-square',
          imagePrompt: '霓虹方块',
          imageSrc: null,
        },
      ],
      holeOptions: [
        {
          holeId: 'hole-square',
          holeKind: 'square',
          label: '方洞',
          imagePrompt: '发光方洞',
          imageSrc: null,
        },
      ],
      backgroundPrompt: '霓虹街机背景',
      coverImageSrc: null,
      backgroundImageSrc: null,
    },
    draft: null,
    messages: [
      {
        id: 'square-hole-message-1',
        role: 'assistant',
        kind: 'chat',
        text: '先确定方洞挑战的题材和反直觉规则。',
        createdAt: '2026-05-01T10:00:00.000Z',
      },
    ],
    lastAssistantReply: '先确定方洞挑战的题材和反直觉规则。',
    publishedProfileId: null,
    updatedAt: '2026-05-01T10:00:00.000Z',
    ...overrides,
  };
}

function buildMockSquareHoleRun(profileId: string) {
  return {
    runId: `square-hole-run-${profileId}`,
    profileId,
    ownerUserId: 'user-2',
    status: 'running',
    snapshotVersion: 1,
    startedAtMs: 1_000,
    durationLimitMs: 600_000,
    remainingMs: 600_000,
    totalShapeCount: 12,
    completedShapeCount: 0,
    combo: 0,
    bestCombo: 0,
    score: 0,
    ruleLabel: '颜色会误导洞口',
    currentShape: {
      shapeId: 'shape-1',
      shapeKind: 'square',
      label: '方块',
      targetHoleId: 'hole-square',
      color: '#ff5f7e',
      imageSrc: null,
    },
    holes: [
      {
        holeId: 'hole-square',
        holeKind: 'square',
        label: '方洞',
        x: 0.2,
        y: 0.5,
      },
    ],
    lastFeedback: null,
  };
}

function buildMockPuzzleRun(
  profileId: string,
  levelName: string,
): PuzzleRunSnapshot {
  const gridSize = 3 as const;

  return {
    runId: `run-${profileId}`,
    entryProfileId: profileId,
    clearedLevelCount: 0,
    currentLevelIndex: 1,
    currentGridSize: gridSize,
    playedProfileIds: [profileId],
    previousLevelTags: ['机关'],
    recommendedNextProfileId: null,
    leaderboardEntries: [],
    currentLevel: {
      runId: `run-${profileId}`,
      levelIndex: 1,
      levelId: 'puzzle-level-1',
      gridSize,
      profileId,
      levelName,
      authorDisplayName: '拼图作者',
      themeTags: ['机关'],
      coverImageSrc: null,
      status: 'playing',
      startedAtMs: 1_000,
      clearedAtMs: null,
      elapsedMs: null,
      timeLimitMs: 300_000,
      remainingMs: 300_000,
      pausedAccumulatedMs: 0,
      pauseStartedAtMs: null,
      freezeAccumulatedMs: 0,
      freezeStartedAtMs: null,
      freezeUntilMs: null,
      leaderboardEntries: [],
      board: {
        rows: 3,
        cols: 3,
        selectedPieceId: null,
        allTilesResolved: false,
        mergedGroups: [],
        pieces: Array.from({ length: 9 }, (_, index) => ({
          pieceId: `piece-${index}`,
          correctRow: Math.floor(index / 3),
          correctCol: index % 3,
          currentRow: Math.floor(index / 3),
          currentCol: index % 3,
          mergedGroupId: null,
        })),
      },
    },
  };
}

function buildPuzzleAnchorPack(): PuzzleAnchorPack {
  return {
    themePromise: {
      key: 'themePromise',
      label: '题材承诺',
      value: '雨夜拼图',
      status: 'confirmed',
    },
    visualSubject: {
      key: 'visualSubject',
      label: '画面主体',
      value: '雨夜猫塔',
      status: 'confirmed',
    },
    visualMood: {
      key: 'visualMood',
      label: '视觉气质',
      value: '暖灯',
      status: 'confirmed',
    },
    compositionHooks: {
      key: 'compositionHooks',
      label: '拼图记忆点',
      value: '灯塔与猫',
      status: 'confirmed',
    },
    tagsAndForbidden: {
      key: 'tagsAndForbidden',
      label: '标签与禁忌',
      value: '雨夜、猫咪、塔',
      status: 'confirmed',
    },
  };
}

function buildMockPuzzleAgentSession(
  overrides: Partial<PuzzleAgentSessionSnapshot> = {},
): PuzzleAgentSessionSnapshot {
  return {
    sessionId: 'puzzle-session-1',
    seedText: '暖灯猫街',
    currentTurn: 0,
    progressPercent: 0,
    stage: 'collecting_anchors',
    anchorPack: buildPuzzleAnchorPack(),
    draft: null,
    messages: [],
    lastAssistantReply: '先说一个你最想做成拼图的画面。',
    publishedProfileId: null,
    suggestedActions: [],
    resultPreview: null,
    updatedAt: '2026-05-14T10:00:00.000Z',
    ...overrides,
  };
}

function buildReadyPuzzleDraft(
  overrides: Partial<PuzzleResultDraft> = {},
): PuzzleResultDraft {
  return {
    workTitle: '自动恢复拼图',
    workDescription: '前端断连后复读 session 恢复的拼图。',
    levelName: '雨夜猫街',
    summary: '屋檐下的猫与暖灯街角。',
    themeTags: ['猫咪', '雨夜', '拼图'],
    forbiddenDirectives: [],
    creatorIntent: null,
    anchorPack: buildPuzzleAnchorPack(),
    candidates: [
      {
        candidateId: 'candidate-1',
        imageSrc: '/puzzle/recovered-candidate.png',
        assetId: 'asset-1',
        prompt: '雨夜猫街',
        actualPrompt: null,
        sourceType: 'generated',
        selected: true,
      },
    ],
    selectedCandidateId: 'candidate-1',
    coverImageSrc: '/puzzle/recovered-candidate.png',
    coverAssetId: 'asset-1',
    generationStatus: 'ready',
    levels: [
      {
        levelId: 'puzzle-level-1',
        levelName: '雨夜猫街',
        pictureDescription: '屋檐下的猫与暖灯街角。',
        pictureReference: null,
        candidates: [
          {
            candidateId: 'candidate-1',
            imageSrc: '/puzzle/recovered-candidate.png',
            assetId: 'asset-1',
            prompt: '雨夜猫街',
            actualPrompt: null,
            sourceType: 'generated',
            selected: true,
          },
        ],
        selectedCandidateId: 'candidate-1',
        coverImageSrc: '/puzzle/recovered-candidate.png',
        coverAssetId: 'asset-1',
        uiBackgroundPrompt: '雨夜猫街竖屏纯背景',
        uiBackgroundImageSrc:
          '/generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
        uiBackgroundImageObjectKey:
          'generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
        levelSceneImageSrc:
          '/generated-puzzle-assets/puzzle-session-recovered/level-scene.png',
        levelSceneImageObjectKey:
          'generated-puzzle-assets/puzzle-session-recovered/level-scene.png',
        uiSpritesheetImageSrc:
          '/generated-puzzle-assets/puzzle-session-recovered/ui-spritesheet.png',
        uiSpritesheetImageObjectKey:
          'generated-puzzle-assets/puzzle-session-recovered/ui-spritesheet.png',
        levelBackgroundImageSrc:
          '/generated-puzzle-assets/puzzle-session-recovered/level-background.png',
        levelBackgroundImageObjectKey:
          'generated-puzzle-assets/puzzle-session-recovered/level-background.png',
        generationStatus: 'ready',
      },
    ],
    ...overrides,
  };
}

function buildClearedPuzzleRun(params: {
  runId: string;
  entryProfileId: string;
  profileId: string;
  levelName: string;
  levelIndex: number;
  elapsedMs: number;
  recommendedNextProfileId?: string | null;
  leaderboardEntries?: PuzzleRunSnapshot['leaderboardEntries'];
}): PuzzleRunSnapshot {
  const baseRun = buildMockPuzzleRun(params.profileId, params.levelName);
  const currentLevel = baseRun.currentLevel!;
  const leaderboardEntries = params.leaderboardEntries ?? [];

  return {
    ...baseRun,
    runId: params.runId,
    entryProfileId: params.entryProfileId,
    currentLevelIndex: params.levelIndex,
    clearedLevelCount: params.levelIndex,
    currentGridSize: currentLevel.gridSize,
    playedProfileIds:
      params.entryProfileId === params.profileId
        ? [params.entryProfileId]
        : [params.entryProfileId, params.profileId],
    recommendedNextProfileId: params.recommendedNextProfileId ?? null,
    leaderboardEntries,
    currentLevel: {
      ...currentLevel,
      runId: params.runId,
      levelIndex: params.levelIndex,
      profileId: params.profileId,
      levelName: params.levelName,
      status: 'cleared',
      clearedAtMs: currentLevel.startedAtMs + params.elapsedMs,
      elapsedMs: params.elapsedMs,
      leaderboardEntries,
      board: {
        ...currentLevel.board,
        allTilesResolved: true,
      },
    },
  };
}

function buildMockMatch3DRun(profileId: string): Match3DRunSnapshot {
  return {
    runId: `match3d-run-${profileId}`,
    profileId,
    ownerUserId: 'user-2',
    status: 'running',
    snapshotVersion: 1,
    startedAtMs: 1_000,
    durationLimitMs: 600_000,
    serverNowMs: 1_000,
    remainingMs: 600_000,
    clearCount: 4,
    totalItemCount: 12,
    clearedItemCount: 0,
    items: [],
    traySlots: Array.from({ length: 7 }, (_, slotIndex) => ({ slotIndex })),
    failureReason: null,
  };
}

const match3DGeneratedUiAsset = {
  prompt: '果园竖屏纯背景',
  imageSrc:
    '/generated-match3d-assets/session/profile/background/background.png',
  imageObjectKey:
    'generated-match3d-assets/session/profile/background/background.png',
  containerPrompt: '果园浅盘容器',
  containerImageSrc:
    '/generated-match3d-assets/session/profile/ui-container/container.png',
  containerImageObjectKey:
    'generated-match3d-assets/session/profile/ui-container/container.png',
  status: 'image_ready',
  error: null,
} satisfies NonNullable<Match3DWorkSummary['generatedBackgroundAsset']>;

function buildMockMatch3DAgentSession(
  overrides: Partial<Match3DAgentSessionSnapshot> = {},
): Match3DAgentSessionSnapshot {
  const sessionId = overrides.sessionId ?? 'match3d-agent-session-1';

  return {
    sessionId,
    currentTurn: 0,
    progressPercent: 20,
    stage: 'collecting',
    anchorPack: {
      theme: {
        key: 'theme',
        label: '题材主题',
        value: '水果消除',
        status: 'confirmed',
      },
      clearCount: {
        key: 'clearCount',
        label: '需要消除次数',
        value: '4',
        status: 'confirmed',
      },
      difficulty: {
        key: 'difficulty',
        label: '难度',
        value: '5',
        status: 'confirmed',
      },
    },
    config: {
      themeText: '水果消除',
      referenceImageSrc: null,
      clearCount: 4,
      difficulty: 5,
    },
    draft: null,
    messages: [
      {
        id: 'match3d-message-1',
        role: 'assistant',
        kind: 'chat',
        text: '我们先确定抓大鹅题材、消除次数和难度。',
        createdAt: '2026-05-01T10:00:00.000Z',
      },
    ],
    lastAssistantReply: '我们先确定抓大鹅题材、消除次数和难度。',
    publishedProfileId: null,
    updatedAt: '2026-05-01T10:00:00.000Z',
    ...overrides,
  };
}

function buildMockRpgGalleryDetail(
  entry: CustomWorldGalleryCard,
): CustomWorldLibraryEntry<CustomWorldProfile> {
  return {
    ...entry,
    profile: {
      id: entry.profileId,
      settingText: entry.summaryText,
      name: entry.worldName,
      subtitle: entry.subtitle,
      summary: entry.summaryText,
      tone: '压抑、潮湿、悬疑',
      playerGoal: '查清旧案。',
      templateWorldType: WorldType.WUXIA,
      attributeSchema: {
        id: `${entry.profileId}-attribute-schema`,
        worldId: entry.profileId,
        schemaVersion: 1,
        generatedFrom: {
          worldType: WorldType.CUSTOM,
          worldName: entry.worldName,
          settingSummary: entry.summaryText,
          tone: '压抑、潮湿、悬疑',
          conflictCore: '雾潮正在逼近港口',
        },
        slots: [],
      },
      majorFactions: ['守灯会'],
      coreConflicts: ['雾潮正在逼近港口'],
      playableNpcs: [],
      storyNpcs: [],
      items: [],
      landmarks: [],
    },
  };
}

const compiledAgentDraftSession: CustomWorldAgentSessionSnapshot = {
  ...mockSession,
  stage: 'object_refining',
  creatorIntent: {
    sourceMode: 'card',
    worldHook: '被海雾吞没的旧航路群岛',
    playerPremise: '玩家回到群岛调查沉船真相。',
    themeKeywords: ['海雾', '旧航路'],
    toneDirectives: ['压抑', '悬疑'],
    openingSituation: '首夜就有陌生船只闯入禁航区。',
    coreConflicts: ['航运公会与守灯会争夺航路控制权'],
    keyFactions: [],
    keyCharacters: [],
    keyLandmarks: [],
    iconicElements: ['会移动的海雾'],
    forbiddenDirectives: [],
    rawSettingText: '',
  },
  draftProfile: {
    name: '潮雾列岛',
    subtitle: '旧灯塔与失控航路',
    summary: '第一版世界底稿已经整理完成。',
    tone: '压抑、潮湿、悬疑',
    playerGoal: '查清沉船与禁航区异动的真相。',
    majorFactions: ['守灯会', '航运公会'],
    coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
    playableNpcs: [
      {
        id: 'playable-1',
        name: '沈砺',
        title: '旧航路引路人',
        role: '关键同行者',
        publicIdentity: '最熟悉旧航路的人。',
        publicMask: '看上去像可靠旧友。',
        currentPressure: '他必须在两股势力间站队。',
        hiddenHook: '暗中替沉船商盟引路。',
        relationToPlayer: '旧友兼潜在背叛者',
        threadIds: ['thread-1'],
        summary: '他像旧友，但也像一把始终没收回鞘的刀。',
      },
    ],
    storyNpcs: [
      {
        id: 'story-1',
        name: '顾潮音',
        title: '守灯会值夜人',
        role: '场景关键角色',
        publicIdentity: '负责夜间巡灯与封锁。',
        publicMask: '对外一直冷静克制。',
        currentPressure: '她知道更多禁航区真相。',
        hiddenHook: '曾亲眼见过失控海雾吞船。',
        relationToPlayer: '最早愿意交换线索的人',
        threadIds: ['thread-1'],
        summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
      },
    ],
    landmarks: [
      {
        id: 'landmark-1',
        name: '回潮旧灯塔',
        purpose: '观察雾潮与往来船只',
        mood: '潮湿、压抑、风声不止',
        importance: '开局核心场景',
        characterIds: ['story-1'],
        threadIds: ['thread-1'],
        summary: '旧灯塔是整片群岛最先看见异动的地方。',
      },
    ],
    factions: [],
    threads: [],
    chapters: [],
    worldHook: '被海雾吞没的旧航路群岛',
    playerPremise: '玩家回到群岛调查沉船真相。',
    openingSituation: '首夜就有陌生船只闯入禁航区。',
    iconicElements: ['会移动的海雾'],
    sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
  },
  draftCards: [
    {
      id: 'world-foundation',
      kind: 'world',
      title: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '第一版世界底稿已经整理完成。',
      status: 'warning',
      linkedIds: ['playable-1', 'story-1', 'landmark-1'],
      warningCount: 0,
    },
  ],
  resultPreview: {
    source: 'session_preview',
    preview: {
      id: 'agent-draft-custom-world-agent-session-1',
      settingText: '被海雾吞没的旧航路群岛',
      name: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '第一版世界底稿已经整理完成。',
      tone: '压抑、潮湿、悬疑',
      playerGoal: '查清沉船与禁航区异动的真相。',
      templateWorldType: 'WUXIA',
      majorFactions: ['守灯会', '航运公会'],
      coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
      playableNpcs: [
        {
          id: 'playable-1',
          name: '沈砺',
          title: '旧航路引路人',
          role: '关键同行者',
          description: '最熟悉旧航路的人。',
          backstory: '曾在沉船夜里带着半支船队逃出海雾。',
          personality: '表面沉稳，心里一直在算退路。',
          motivation: '想赶在守灯会封航前查清真相。',
          combatStyle: '借地形和潮路换位，先拉扯再压近。',
          initialAffinity: 18,
          relationshipHooks: ['旧友', '沉船旧案'],
          tags: ['潮路', '引路'],
        },
      ],
      storyNpcs: [
        {
          id: 'story-1',
          name: '顾潮音',
          title: '守灯会值夜人',
          role: '场景关键角色',
          description: '夜里巡灯与封锁禁航区的人。',
          backstory: '在失控海雾第一次吞船那夜，她是最后一个还留在灯塔顶的人。',
          personality: '冷静克制，但提到旧灯册时会显得过分警觉。',
          motivation: '想守住灯塔记录，也想找到谁先改动了禁航信号。',
          combatStyle: '借塔顶视角和风向压制，再用灯火错位扰乱。',
          initialAffinity: 8,
          relationshipHooks: ['禁航记录', '灯塔值夜'],
          tags: ['守灯会', '灯塔'],
        },
      ],
      items: [],
      landmarks: [
        {
          id: 'landmark-1',
          name: '回潮旧灯塔',
          description: '旧灯塔是整片群岛最先看见异动的地方。',
          sceneNpcIds: ['story-1'],
          connections: [],
        },
      ],
      generationMode: 'full',
      generationStatus: 'complete',
      sessionId: 'custom-world-agent-session-1',
    },
    generatedAt: '2026-04-14T12:00:00.000Z',
    qualityFindings: [],
    blockers: [],
    publishReady: true,
    canEnterWorld: false,
  },
};

const compiledAgentResultPreview = normalizeCustomWorldProfileRecord(
  compiledAgentDraftSession.resultPreview?.preview,
);

if (!compiledAgentResultPreview) {
  throw new Error('failed to normalize compiled agent result preview');
}

function buildResultViewForSession(
  session: CustomWorldAgentSessionSnapshot,
): RpgCreationResultView {
  const profile = session.resultPreview?.preview ?? null;
  const isResultStage =
    session.stage === 'object_refining' ||
    session.stage === 'visual_refining' ||
    session.stage === 'long_tail_review' ||
    session.stage === 'ready_to_publish' ||
    session.stage === 'published';

  return {
    session,
    profile,
    profileSource: profile ? 'result_preview' : 'none',
    targetStage:
      profile && isResultStage
        ? 'custom-world-result'
        : session.stage === 'error'
          ? 'custom-world-generating'
          : 'agent-workspace',
    generationViewSource:
      session.stage === 'error' ? 'agent-draft-foundation' : null,
    resultViewSource: profile && isResultStage ? 'agent-draft' : null,
    canAutosaveLibrary: Boolean(profile && isResultStage),
    canSyncResultProfile:
      session.stage === 'object_refining' ||
      session.stage === 'visual_refining' ||
      session.stage === 'long_tail_review' ||
      session.stage === 'ready_to_publish',
    publishReady: Boolean(session.resultPreview?.publishReady),
    canEnterWorld: Boolean(session.resultPreview?.canEnterWorld),
    blockerCount: session.resultPreview?.blockers?.length ?? 0,
    recoveryAction:
      profile && isResultStage
        ? 'open_result'
        : session.stage === 'error'
          ? 'resume_generation'
          : 'continue_agent',
    recoveryReason: null,
  };
}

function buildExistingRpgDraftWork(
  overrides: Partial<CustomWorldWorkSummary> = {},
): CustomWorldWorkSummary {
  return {
    workId: 'draft:custom-world-agent-session-1',
    sourceType: 'agent_session',
    status: 'draft',
    title: '潮雾列岛',
    subtitle: '待完善草稿',
    summary: '玩家是失职返乡的守灯人。',
    coverImageSrc: null,
    coverRenderMode: 'image',
    coverCharacterImageSrcs: [],
    updatedAt: '2026-04-20T10:00:00.000Z',
    publishedAt: null,
    stage: 'object_refining',
    stageLabel: '待完善草稿',
    playableNpcCount: 3,
    landmarkCount: 4,
    roleVisualReadyCount: 1,
    roleAnimationReadyCount: 0,
    roleAssetSummaryLabel: '沈砺 · 主图已生成',
    sessionId: 'custom-world-agent-session-1',
    profileId: null,
    canResume: true,
    canEnterWorld: false,
    ...overrides,
  };
}

function mockExistingRpgDraftShelf(
  overrides: Partial<CustomWorldWorkSummary> = {},
) {
  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    buildExistingRpgDraftWork(overrides),
  ]);
}

type TestAuthValue = {
  user: AuthUser | null;
  canAccessProtectedData: boolean;
  openLoginModal: (postLoginAction?: (() => void) | null) => void;
  requireAuth: (action: () => void) => void;
  openSettingsModal: (section?: PlatformSettingsSection) => void;
  openAccountModal: () => void;
  setCurrentUser: (user: AuthUser) => void;
  logout: () => Promise<void>;
  musicVolume: number;
  setMusicVolume: (value: number) => void;
  platformTheme: 'light' | 'dark';
  setPlatformTheme: (theme: 'light' | 'dark') => void;
  isHydratingSettings: boolean;
  isPersistingSettings: boolean;
  settingsError: string | null;
};

function createAuthValue(
  overrides: Partial<TestAuthValue> = {},
): TestAuthValue {
  return {
    user: mockAuthUser,
    canAccessProtectedData: true,
    openLoginModal: () => {},
    requireAuth: (action) => action(),
    openSettingsModal: () => {},
    openAccountModal: () => {},
    setCurrentUser: () => {},
    logout: async () => {},
    musicVolume: 0.42,
    setMusicVolume: () => {},
    platformTheme: 'light',
    setPlatformTheme: () => {},
    isHydratingSettings: false,
    isPersistingSettings: false,
    settingsError: null,
    ...overrides,
  };
}

function TestWrapper({
  withAuth = false,
  authValue,
  onContinueGame,
  onSelectWorld,
}: {
  withAuth?: boolean;
  authValue?: TestAuthValue;
  onContinueGame?: (snapshot?: HydratedSavedGameSnapshot | null) => void;
  onSelectWorld?: RpgEntryFlowShellProps['handleCustomWorldSelect'];
} = {}) {
  const [selectionStage, setSelectionStage] = useState<SelectionStage>(() =>
    resolveSelectionStageFromPath(window.location.pathname),
  );
  const [initialPublicWorkCode] = useState(() =>
    readPublicWorkCodeFromLocationSearch(window.location.search),
  );

  const content = (
    <RpgEntryFlowShell
      selectionStage={selectionStage}
      setSelectionStage={setSelectionStage}
      initialPublicWorkCode={initialPublicWorkCode}
      hasSavedGame={false}
      savedSnapshot={null}
      handleContinueGame={onContinueGame ?? (() => {})}
      handleStartNewGame={() => {}}
      handleCustomWorldSelect={onSelectWorld ?? (() => {})}
    />
  );

  if (!withAuth && !authValue) {
    return content;
  }

  return (
    <AuthUiContext.Provider value={authValue ?? createAuthValue()}>
      {content}
    </AuthUiContext.Provider>
  );
}

beforeEach(() => {
  vi.resetAllMocks();
  vi.mocked(authServiceMocks.ensureRuntimeGuestToken).mockResolvedValue({
    token: 'runtime-guest-token',
    expiresAt: '2099-01-01T00:00:00.000Z',
  });
  vi.mocked(
    match3dGeneratedModelCache.hasMatch3DGeneratedImageAsset,
  ).mockImplementation((assets) =>
    Boolean(
      assets?.some(
        (asset) =>
          asset.imageSrc?.trim() ||
          asset.imageObjectKey?.trim() ||
          asset.imageViews?.some(
            (view) => view.imageSrc?.trim() || view.imageObjectKey?.trim(),
          ),
      ),
    ),
  );
  vi.mocked(
    match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime,
  ).mockImplementation((assets) => {
    if (!assets?.length) {
      return [];
    }
    const musicCarrier = assets.find((asset) =>
      asset.backgroundMusic?.audioSrc?.trim(),
    );
    if (!musicCarrier) {
      return [...assets];
    }
    return assets.map((asset, index) =>
      index === 0
        ? {
            ...asset,
            backgroundMusic:
              asset.backgroundMusic ?? musicCarrier.backgroundMusic,
          }
        : {
            ...asset,
            backgroundMusic: null,
            backgroundMusicTitle: null,
            backgroundMusicStyle: null,
            backgroundMusicPrompt: null,
          },
    );
  });
  vi.mocked(
    match3dGeneratedModelCache.mergeMatch3DGeneratedItemAssetsForRuntime,
  ).mockImplementation((primaryAssets, fallbackAssets) => {
    const primary = primaryAssets ?? [];
    const fallback = fallbackAssets ?? [];
    if (primary.length <= 0) {
      return match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime(
        fallback,
      );
    }
    if (fallback.length <= 0) {
      return match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime(
        primary,
      );
    }
    const fallbackById = new Map(
      fallback.map((asset) => [asset.itemId, asset]),
    );
    return match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime(
      primary.map((asset) => {
        const fallbackAsset = fallbackById.get(asset.itemId);
        return fallbackAsset
          ? {
              ...asset,
              imageSrc: asset.imageSrc ?? fallbackAsset.imageSrc ?? null,
              imageObjectKey:
                asset.imageObjectKey ?? fallbackAsset.imageObjectKey ?? null,
              imageViews:
                asset.imageViews && asset.imageViews.length > 0
                  ? asset.imageViews
                  : (fallbackAsset.imageViews ?? []),
              backgroundMusic:
                asset.backgroundMusic ?? fallbackAsset.backgroundMusic ?? null,
              backgroundAsset:
                asset.backgroundAsset ?? fallbackAsset.backgroundAsset ?? null,
            }
          : asset;
      }),
    );
  });
  vi.mocked(
    match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
  ).mockResolvedValue(undefined);
  vi.mocked(createServerMatch3DRuntimeAdapter).mockReturnValue(
    match3dServerRuntimeAdapterMock,
  );
  match3dServerRuntimeAdapterMock.startRun.mockRejectedValue(
    new Error('未启动抓大鹅运行态'),
  );
  match3dServerRuntimeAdapterMock.clickItem.mockRejectedValue(
    new Error('未执行抓大鹅点击'),
  );
  match3dServerRuntimeAdapterMock.restartRun.mockRejectedValue(
    new Error('未重新开始抓大鹅运行态'),
  );
  match3dServerRuntimeAdapterMock.finishTimeUp.mockResolvedValue({
    run: buildMockMatch3DRun('match3d-profile-time-up'),
  });
  match3dServerRuntimeAdapterMock.stopRun.mockResolvedValue({
    run: buildMockMatch3DRun('match3d-profile-stopped'),
  });
  window.history.replaceState(null, '', '/');
  window.sessionStorage.clear();
  window.localStorage.clear();
  window.localStorage.setItem(
    'genarrative.puzzle-onboarding.first-visit.v1',
    '1',
  );
  vi.mocked(fetchCreationEntryConfig).mockResolvedValue(
    testCreationEntryConfig,
  );
  vi.mocked(getProfileDashboard).mockResolvedValue({
    walletBalance: 20,
    totalPlayTimeMs: 0,
    playedWorldCount: 0,
    updatedAt: '2026-04-16T12:00:00.000Z',
  });
  vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([]);
  vi.mocked(listRpgEntryWorldGallery).mockResolvedValue([]);
  vi.mocked(getRpgEntryWorldGalleryDetail).mockImplementation(
    async (ownerUserId, profileId) =>
      buildMockRpgGalleryDetail({
        ownerUserId,
        profileId,
        publicWorkCode: null,
        authorPublicUserCode: ownerUserId,
        visibility: 'published',
        publishedAt: '2026-04-16T12:00:00.000Z',
        updatedAt: '2026-04-16T12:00:00.000Z',
        authorDisplayName: '测试作者',
        worldName: '潮雾列岛',
        subtitle: '旧灯塔与失控航路',
        summaryText: '最近公开发布的世界。',
        coverImageSrc: null,
        themeMode: 'tide',
        playableNpcCount: 0,
        landmarkCount: 0,
        likeCount: 0,
      }),
  );
  vi.mocked(getRpgEntryWorldGalleryDetailFromClient).mockImplementation(
    async (ownerUserId, profileId) =>
      buildMockRpgGalleryDetail({
        ownerUserId,
        profileId,
        publicWorkCode: null,
        authorPublicUserCode: ownerUserId,
        visibility: 'published',
        publishedAt: '2026-04-16T12:00:00.000Z',
        updatedAt: '2026-04-16T12:00:00.000Z',
        authorDisplayName: '测试作者',
        worldName: '潮雾列岛',
        subtitle: '旧灯塔与失控航路',
        summaryText: '最近公开发布的世界。',
        coverImageSrc: null,
        themeMode: 'tide',
        playableNpcCount: 0,
        landmarkCount: 0,
        likeCount: 0,
      }),
  );
  vi.mocked(
    rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
  ).mockImplementation(async (profileId: string) =>
    buildMockRpgGalleryDetail({
      ownerUserId: mockAuthUser.id,
      profileId,
      publicWorkCode: `work-${profileId}`,
      authorPublicUserCode: mockAuthUser.publicUserCode,
      visibility: 'published',
      publishedAt: '2026-04-16T12:00:00.000Z',
      updatedAt: '2026-04-16T12:00:00.000Z',
      authorDisplayName: mockAuthUser.displayName,
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '最近公开发布的世界。',
      coverImageSrc: null,
      themeMode: 'tide',
      playableNpcCount: 0,
      landmarkCount: 0,
      likeCount: 0,
    }),
  );
  vi.mocked(listProfileBrowseHistory).mockResolvedValue([]);
  vi.mocked(listProfileSaveArchives).mockResolvedValue([]);
  vi.mocked(resumeProfileSaveArchive).mockResolvedValue({
    entry: {
      worldKey: 'custom:world-archive-1',
      ownerUserId: null,
      profileId: 'world-archive-1',
      worldType: 'CUSTOM',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '回到旧灯塔继续推进调查。',
      coverImageSrc: null,
      lastPlayedAt: '2026-04-19T12:00:00.000Z',
    },
    snapshot: {
      version: 2,
      savedAt: '2026-04-19T12:00:00.000Z',
      bottomTab: 'adventure',
      currentStory: null,
      gameState: {},
    } as HydratedSavedGameSnapshot,
  });
  vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
  vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
  vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);
  vi.mocked(deleteBarkBattleWork).mockResolvedValue({ items: [] });
  vi.mocked(listVisualNovelGallery).mockResolvedValue({ works: [] });
  vi.mocked(listVisualNovelWorks).mockResolvedValue({ works: [] });
  vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
    items: [],
    hasMore: false,
    nextCursor: null,
  });
  vi.mocked(woodenFishClient.listWorks).mockResolvedValue({ items: [] });
  vi.mocked(woodenFishClient.deleteWork).mockResolvedValue({ items: [] });
  vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([]);
  vi.mocked(deleteLocalBabyObjectMatchDraft).mockResolvedValue([]);
  vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
    items: [],
    hasMore: false,
    nextCursor: null,
  });
  vi.mocked(jumpHopClient.listWorks).mockResolvedValue({ items: [] });
  vi.mocked(jumpHopClient.deleteWork).mockResolvedValue({ items: [] });
  vi.mocked(jumpHopClient.getSession).mockRejectedValue(
    new Error('未找到跳一跳会话'),
  );
  vi.mocked(jumpHopClient.getWorkDetail).mockRejectedValue(
    new Error('未找到跳一跳作品'),
  );
  vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
    items: [],
    hasMore: false,
    nextCursor: null,
  });
  vi.mocked(woodenFishClient.listWorks).mockResolvedValue({ items: [] });
  vi.mocked(woodenFishClient.deleteWork).mockResolvedValue({ items: [] });
  vi.mocked(woodenFishClient.getSession).mockRejectedValue(
    new Error('未找到敲木鱼会话'),
  );
  vi.mocked(woodenFishClient.getWorkDetail).mockRejectedValue(
    new Error('未找到敲木鱼作品'),
  );
  vi.mocked(saveBabyObjectMatchDraft).mockImplementation(async (payload) => ({
    draft: payload.draft,
  }));
  vi.mocked(regenerateBabyObjectMatchDraftAssets).mockImplementation(
    async (draft) => ({ draft }),
  );
  vi.mocked(publishBabyObjectMatchWork).mockImplementation(async (payload) => ({
    draft: {
      ...payload.draft,
      publicationStatus: 'published',
      publishedAt: '2026-05-14T10:10:00.000Z',
    },
    publicWorkCode: `BO-${payload.draft.profileId}`,
  }));
  vi.mocked(recordBigFishPlay).mockResolvedValue({ items: [] });
  vi.mocked(recordRpgEntryWorldGalleryPlay).mockImplementation(
    async (ownerUserId, profileId) => ({
      ownerUserId,
      profileId,
      publicWorkCode: null,
      authorPublicUserCode: ownerUserId,
      profile: {
        id: profileId,
        name: '潮雾列岛',
        subtitle: '旧灯塔与失控航路',
        summary: '最近公开发布的世界。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清旧案。',
        majorFactions: ['守灯会'],
        coreConflicts: ['雾潮正在逼近港口'],
        playableNpcs: [],
        storyNpcs: [],
        landmarks: [],
      } as never,
      visibility: 'published',
      publishedAt: '2026-04-16T12:00:00.000Z',
      updatedAt: '2026-04-16T12:00:00.000Z',
      authorDisplayName: '测试作者',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '最近公开发布的世界。',
      coverImageSrc: null,
      themeMode: 'tide',
      playableNpcCount: 0,
      landmarkCount: 0,
      likeCount: 0,
    }),
  );
  vi.mocked(remixRpgEntryWorldGallery).mockRejectedValue(
    new Error('未启用 remix'),
  );
  vi.mocked(getRpgEntryWorldGalleryDetailByCode).mockRejectedValue(
    new Error('未找到公开作品'),
  );
  vi.mocked(upsertRpgWorldProfile).mockResolvedValue({
    entry: {
      ownerUserId: 'user-1',
      profileId: 'agent-draft-custom-world-agent-session-1',
      publicWorkCode: null,
      authorPublicUserCode: null,
      profile: {
        id: 'agent-draft-custom-world-agent-session-1',
        name: '潮雾列岛',
      } as never,
      visibility: 'draft',
      publishedAt: null,
      updatedAt: '2026-04-14T12:00:00.000Z',
      authorDisplayName: '玩家',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '第一版世界底稿已经整理完成。',
      coverImageSrc: null,
      themeMode: 'tide',
      playableNpcCount: 1,
      landmarkCount: 1,
      likeCount: 0,
    },
    entries: [],
  });
  vi.mocked(createRpgCreationSession).mockResolvedValue({
    session: mockSession,
  });
  vi.mocked(getRpgCreationResultView).mockImplementation(async () =>
    buildResultViewForSession(mockSession),
  );
  vi.mocked(createBigFishCreationSession).mockResolvedValue({
    session: {
      sessionId: 'big-fish-session-1',
      currentTurn: 0,
      progressPercent: 0,
      stage: 'clarifying',
      anchorPack: {
        gameplayPromise: {
          key: 'gameplay_promise',
          label: '核心玩法',
          value: '',
          status: 'missing',
        },
        ecologyVisualTheme: {
          key: 'ecology_visual_theme',
          label: '生态视觉',
          value: '',
          status: 'missing',
        },
        growthLadder: {
          key: 'growth_ladder',
          label: '成长阶梯',
          value: '',
          status: 'missing',
        },
        riskTempo: {
          key: 'risk_tempo',
          label: '风险节奏',
          value: '',
          status: 'missing',
        },
      },
      draft: null,
      assetSlots: [],
      assetCoverage: {
        levelMainImageReadyCount: 0,
        levelMotionReadyCount: 0,
        backgroundReady: false,
        requiredLevelCount: 0,
        publishReady: false,
        blockers: [],
      },
      messages: [],
      lastAssistantReply: '先说说你想要什么样的大鱼生态。',
      publishReady: false,
      updatedAt: '2026-04-22T12:00:00.000Z',
    },
  });
  vi.mocked(getBigFishCreationSession).mockResolvedValue({
    session: {
      sessionId: 'big-fish-session-1',
      currentTurn: 2,
      progressPercent: 90,
      stage: 'draft_ready',
      anchorPack: {
        gameplayPromise: {
          key: 'gameplay_promise',
          label: '核心玩法',
          value: '机械微生物吞并进化',
          status: 'confirmed',
        },
        ecologyVisualTheme: {
          key: 'ecology_visual_theme',
          label: '生态视觉',
          value: '深海机械浮游生态',
          status: 'confirmed',
        },
        growthLadder: {
          key: 'growth_ladder',
          label: '成长阶梯',
          value: '从微光孢子到深海巨鲲',
          status: 'confirmed',
        },
        riskTempo: {
          key: 'risk_tempo',
          label: '风险节奏',
          value: '快节奏吞并，后段压迫感增强',
          status: 'confirmed',
        },
      },
      draft: {
        title: '机械深海 大鱼吃小鱼',
        subtitle: '机械微生物吞并进化 · 偏爽快节奏',
        coreFun: '吞并更小机械生命并持续合体成长',
        ecologyTheme: '深海机械浮游生态',
        levels: [
          {
            level: 1,
            name: '微光孢子',
            oneLineFantasy: '像发光尘埃一样在深海漂浮。',
            textDescription:
              '微光孢子是机械深海生态中的起始个体，体型最小，会先漂浮试探并寻找可吞并目标。',
            silhouetteDirection: '圆润微型机械球',
            sizeRatio: 1,
            visualDescription:
              '带有浅色发光核心的微型机械鱼苗或孢子体，轮廓圆润，表现出弱小但灵动的初始形象。',
            visualPromptSeed: 'deep sea glowing mechanical spore',
            idleMotionDescription:
              '待机时轻轻漂浮，身体和尾部做小幅摆动，像在适应深海水流。',
            moveMotionDescription:
              '移动时核心前探，尾部快速摆动推进，带出轻盈的游动轨迹。',
            motionPromptSeed: 'soft floating mechanical spore',
            mergeSourceLevel: null,
            preyWindow: [1],
            threatWindow: [2],
            isFinalLevel: false,
          },
        ],
        background: {
          theme: '机械深海',
          colorMood: '冷青色与暗金反光',
          foregroundHints: '漂浮齿轮碎片',
          midgroundComposition: '珊瑚状机械群落',
          backgroundDepth: '深海远景光柱',
          safePlayAreaHint: '中心区域留空',
          spawnEdgeHint: '边缘暗流刷怪',
          backgroundPromptSeed: 'mechanical deep sea arena',
        },
        runtimeParams: {
          levelCount: 8,
          mergeCountPerUpgrade: 3,
          spawnTargetCount: 28,
          leaderMoveSpeed: 1.2,
          followerCatchUpSpeed: 1,
          offscreenCullSeconds: 8,
          preySpawnDeltaLevels: [-2, -1],
          threatSpawnDeltaLevels: [1, 2],
          winLevel: 8,
        },
      },
      assetSlots: [],
      assetCoverage: {
        levelMainImageReadyCount: 0,
        levelMotionReadyCount: 0,
        backgroundReady: false,
        requiredLevelCount: 8,
        publishReady: false,
        blockers: ['仍有主图、动作和背景未生成'],
      },
      messages: [
        {
          id: 'big-fish-message-1',
          role: 'assistant',
          kind: 'chat',
          text: '先说说你想要什么样的大鱼生态。',
          createdAt: '2026-04-22T12:00:00.000Z',
        },
        {
          id: 'big-fish-message-2',
          role: 'user',
          kind: 'chat',
          text: '我想做机械深海里微生物互相吞并进化。',
          createdAt: '2026-04-22T12:01:00.000Z',
        },
      ],
      lastAssistantReply: '大鱼结果页草稿已经生成，可以补正式资产。',
      publishReady: false,
      updatedAt: '2026-04-22T12:10:00.000Z',
    },
  });
  vi.mocked(createPuzzleAgentSession).mockResolvedValue({
    session: {
      sessionId: 'puzzle-session-1',
      currentTurn: 0,
      progressPercent: 0,
      stage: 'collecting_anchors',
      anchorPack: {
        themePromise: {
          key: 'theme_promise',
          label: '主题承诺',
          value: '',
          status: 'missing',
        },
        visualSubject: {
          key: 'visual_subject',
          label: '视觉主体',
          value: '',
          status: 'missing',
        },
        visualMood: {
          key: 'visual_mood',
          label: '视觉气质',
          value: '',
          status: 'missing',
        },
        compositionHooks: {
          key: 'composition_hooks',
          label: '构图钩子',
          value: '',
          status: 'missing',
        },
        tagsAndForbidden: {
          key: 'tags_and_forbidden',
          label: '标签与禁区',
          value: '',
          status: 'missing',
        },
      },
      draft: null,
      messages: [],
      lastAssistantReply: '先说一个你最想做成拼图的画面。',
      publishedProfileId: null,
      suggestedActions: [],
      resultPreview: null,
      updatedAt: '2026-04-22T12:00:00.000Z',
    },
  });
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: {
      sessionId: 'puzzle-session-1',
      currentTurn: 3,
      progressPercent: 88,
      stage: 'draft_ready',
      anchorPack: {
        themePromise: {
          key: 'theme_promise',
          label: '主题承诺',
          value: '雨夜遗迹探索',
          status: 'confirmed',
        },
        visualSubject: {
          key: 'visual_subject',
          label: '视觉主体',
          value: '发光猫咪站在遗迹台阶上',
          status: 'confirmed',
        },
        visualMood: {
          key: 'visual_mood',
          label: '视觉气质',
          value: '潮湿、梦幻、轻悬疑',
          status: 'confirmed',
        },
        compositionHooks: {
          key: 'composition_hooks',
          label: '构图钩子',
          value: '台阶透视、倒影、门洞',
          status: 'confirmed',
        },
        tagsAndForbidden: {
          key: 'tags_and_forbidden',
          label: '标签与禁区',
          value: '雨夜、猫咪、遗迹；禁止文字水印',
          status: 'confirmed',
        },
      },
      draft: {
        levelName: '雨夜猫塔',
        summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
        themeTags: ['雨夜', '猫咪', '遗迹'],
        forbiddenDirectives: ['文字水印'],
        creatorIntent: null,
        anchorPack: {
          themePromise: {
            key: 'theme_promise',
            label: '主题承诺',
            value: '雨夜遗迹探索',
            status: 'confirmed',
          },
          visualSubject: {
            key: 'visual_subject',
            label: '视觉主体',
            value: '发光猫咪站在遗迹台阶上',
            status: 'confirmed',
          },
          visualMood: {
            key: 'visual_mood',
            label: '视觉气质',
            value: '潮湿、梦幻、轻悬疑',
            status: 'confirmed',
          },
          compositionHooks: {
            key: 'composition_hooks',
            label: '构图钩子',
            value: '台阶透视、倒影、门洞',
            status: 'confirmed',
          },
          tagsAndForbidden: {
            key: 'tags_and_forbidden',
            label: '标签与禁区',
            value: '雨夜、猫咪、遗迹；禁止文字水印',
            status: 'confirmed',
          },
        },
        candidates: [],
        selectedCandidateId: null,
        coverImageSrc: null,
        coverAssetId: null,
        generationStatus: 'idle',
      },
      messages: [
        {
          id: 'puzzle-message-1',
          role: 'assistant',
          kind: 'chat',
          text: '先说一个你最想做成拼图的画面。',
          createdAt: '2026-04-22T12:00:00.000Z',
        },
        {
          id: 'puzzle-message-2',
          role: 'user',
          kind: 'chat',
          text: '雨夜里有一只会发光的猫站在遗迹台阶上。',
          createdAt: '2026-04-22T12:01:00.000Z',
        },
      ],
      lastAssistantReply: '拼图结果页草稿已经生成，可以开始出图并确认标签。',
      publishedProfileId: null,
      suggestedActions: [],
      resultPreview: {
        draft: {
          levelName: '雨夜猫塔',
          summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
          themeTags: ['雨夜', '猫咪', '遗迹'],
          forbiddenDirectives: ['文字水印'],
          creatorIntent: null,
          anchorPack: {
            themePromise: {
              key: 'theme_promise',
              label: '主题承诺',
              value: '雨夜遗迹探索',
              status: 'confirmed',
            },
            visualSubject: {
              key: 'visual_subject',
              label: '视觉主体',
              value: '发光猫咪站在遗迹台阶上',
              status: 'confirmed',
            },
            visualMood: {
              key: 'visual_mood',
              label: '视觉气质',
              value: '潮湿、梦幻、轻悬疑',
              status: 'confirmed',
            },
            compositionHooks: {
              key: 'composition_hooks',
              label: '构图钩子',
              value: '台阶透视、倒影、门洞',
              status: 'confirmed',
            },
            tagsAndForbidden: {
              key: 'tags_and_forbidden',
              label: '标签与禁区',
              value: '雨夜、猫咪、遗迹；禁止文字水印',
              status: 'confirmed',
            },
          },
          candidates: [],
          selectedCandidateId: null,
          coverImageSrc: null,
          coverAssetId: null,
          generationStatus: 'idle',
        },
        blockers: [
          {
            id: 'missing-cover-image',
            code: 'MISSING_COVER_IMAGE',
            message: '正式拼图图片尚未确定',
          },
        ],
        qualityFindings: [],
        publishReady: false,
      },
      updatedAt: '2026-04-22T12:10:00.000Z',
    },
  });
  vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
  vi.mocked(listRpgCreationWorks).mockResolvedValue([]);
  vi.mocked(listBigFishWorks).mockResolvedValue({
    items: [],
  });
  vi.mocked(listBigFishGallery).mockResolvedValue({
    items: [],
  });
  vi.mocked(startBigFishRun).mockResolvedValue({
    run: {
      runId: 'big-fish-run-1',
      sessionId: 'big-fish-session-public-1',
      status: 'running',
      tick: 0,
      playerLevel: 1,
      winLevel: 8,
      leaderEntityId: 'owned-1',
      ownedEntities: [
        {
          entityId: 'owned-1',
          level: 1,
          position: { x: 0, y: 0 },
          radius: 12,
          offscreenSeconds: 0,
        },
      ],
      wildEntities: [],
      cameraCenter: { x: 0, y: 0 },
      lastInput: { x: 0, y: 0 },
      eventLog: ['机械鱼群开始巡游。'],
      updatedAt: '2026-04-25T12:12:00.000Z',
    },
  });
  vi.mocked(submitBigFishInput).mockImplementation(async (runId, payload) => ({
    run: {
      runId,
      sessionId: 'big-fish-session-public-1',
      status: 'running',
      tick: 1,
      playerLevel: 1,
      winLevel: 8,
      leaderEntityId: 'owned-1',
      ownedEntities: [
        {
          entityId: 'owned-1',
          level: 1,
          position: payload,
          radius: 12,
          offscreenSeconds: 0,
        },
      ],
      wildEntities: [],
      cameraCenter: payload,
      lastInput: payload,
      eventLog: ['机械鱼群继续巡游。'],
      updatedAt: '2026-04-25T12:12:01.000Z',
    },
  }));
  vi.mocked(recordBigFishPlay).mockResolvedValue({ items: [] });
  vi.mocked(createBarkBattleDraft).mockResolvedValue({
    draftId: 'bark-battle-draft-1',
    workId: 'bark-battle-work-1',
    title: '汪汪测试杯',
    description: '',
    themeDescription: '阳光草坪声浪竞技场',
    playerImageDescription: '戴红色围巾的柯基选手',
    opponentImageDescription: '蓝色护目镜哈士奇对手',
    difficultyPreset: 'normal',
    configVersion: 1,
    rulesetVersion: 'bark-battle-ruleset-v1',
    updatedAt: '2026-05-14T10:00:00.000Z',
  });
  vi.mocked(generateAllBarkBattleImageAssets).mockResolvedValue({
    assets: {
      'player-character': {
        imageSrc: '/generated-bark-battle/player.png',
        assetId: 'asset-player',
        model: 'gpt-image-2-all',
        size: '1024*1024',
        taskId: 'task-player',
        prompt: 'player',
      },
      'opponent-character': {
        imageSrc: '/generated-bark-battle/opponent.png',
        assetId: 'asset-opponent',
        model: 'gpt-image-2-all',
        size: '1024*1024',
        taskId: 'task-opponent',
        prompt: 'opponent',
      },
      'ui-background': {
        imageSrc: '/generated-bark-battle/background.png',
        assetId: 'asset-background',
        model: 'gpt-image-2-all',
        size: '1024*1792',
        taskId: 'task-background',
        prompt: 'background',
      },
    },
    failures: {},
  });
  vi.mocked(updateBarkBattleDraftConfig).mockImplementation(
    async (payload) => ({
      draftId: payload.draftId,
      workId: payload.workId ?? 'bark-battle-work-1',
      title: payload.title,
      description: payload.description,
      themeDescription: payload.themeDescription,
      playerImageDescription: payload.playerImageDescription,
      opponentImageDescription: payload.opponentImageDescription,
      playerCharacterImageSrc: payload.playerCharacterImageSrc,
      opponentCharacterImageSrc: payload.opponentCharacterImageSrc,
      uiBackgroundImageSrc: payload.uiBackgroundImageSrc,
      difficultyPreset: payload.difficultyPreset,
      configVersion: (payload.configVersion ?? 1) + 1,
      rulesetVersion: payload.rulesetVersion ?? 'bark-battle-ruleset-v1',
      updatedAt: '2026-05-14T10:01:00.000Z',
    }),
  );
  vi.mocked(listBarkBattleWorks).mockResolvedValue({ items: [] });
  vi.mocked(listBarkBattleGallery).mockResolvedValue({ items: [] });
  vi.mocked(publishBarkBattleWork).mockResolvedValue({
    workId: 'bark-battle-work-1',
    draftId: 'bark-battle-draft-1',
    configVersion: 1,
    rulesetVersion: 'bark-battle-ruleset-v1',
    playTypeId: 'bark-battle',
    title: '汪汪测试杯',
    description: '',
    themeDescription: '阳光草坪声浪竞技场',
    playerImageDescription: '戴红色围巾的柯基选手',
    opponentImageDescription: '蓝色护目镜哈士奇对手',
    difficultyPreset: 'normal',
    updatedAt: '2026-05-14T10:00:00.000Z',
    publishedAt: '2026-05-14T10:00:00.000Z',
  });
  vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
    session: buildMockMatch3DAgentSession(),
  });
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: buildMockMatch3DAgentSession(),
  });
  vi.mocked(match3dCreationClient.streamMessage).mockResolvedValue(
    buildMockMatch3DAgentSession(),
  );
  vi.mocked(match3dCreationClient.executeAction).mockResolvedValue({
    session: buildMockMatch3DAgentSession(),
  });
  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [],
  });
  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [],
  });
  vi.mocked(getMatch3DWorkDetail).mockRejectedValue(
    new Error('未找到抓大鹅作品'),
  );
  vi.mocked(deleteMatch3DWork).mockResolvedValue({
    items: [],
  });
  vi.mocked(squareHoleCreationClient.createSession).mockResolvedValue({
    session: buildMockSquareHoleAgentSession(),
  });
  vi.mocked(squareHoleCreationClient.getSession).mockResolvedValue({
    session: buildMockSquareHoleAgentSession(),
  });
  vi.mocked(squareHoleCreationClient.streamMessage).mockResolvedValue(
    buildMockSquareHoleAgentSession(),
  );
  vi.mocked(squareHoleCreationClient.executeAction).mockResolvedValue({
    session: buildMockSquareHoleAgentSession(),
  });
  vi.mocked(listSquareHoleWorks).mockResolvedValue({
    items: [],
  });
  vi.mocked(listSquareHoleGallery).mockResolvedValue({
    items: [],
  });
  vi.mocked(getSquareHoleWorkDetail).mockRejectedValue(
    new Error('未找到方洞挑战作品'),
  );
  vi.mocked(deleteSquareHoleWork).mockResolvedValue({
    items: [],
  });
  vi.mocked(startSquareHoleRun).mockResolvedValue({
    run: buildMockSquareHoleRun('square-hole-profile-1'),
  });
  vi.mocked(dropSquareHoleShape).mockResolvedValue({
    feedback: {
      accepted: true,
      rejectReason: null,
      message: '投入成功',
    },
    run: buildMockSquareHoleRun('square-hole-profile-1'),
  });
  vi.mocked(restartSquareHoleRun).mockResolvedValue({
    run: buildMockSquareHoleRun('square-hole-profile-1'),
  });
  vi.mocked(finishSquareHoleTimeUp).mockResolvedValue({
    run: buildMockSquareHoleRun('square-hole-profile-1'),
  });
  vi.mocked(stopSquareHoleRun).mockResolvedValue({
    run: buildMockSquareHoleRun('square-hole-profile-1'),
  });
  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [],
  });
  vi.mocked(updatePuzzleWork).mockImplementation(
    async (profileId, payload) => ({
      item: {
        workId: `puzzle-work-${profileId}`,
        profileId,
        ownerUserId: mockAuthUser.id,
        sourceSessionId: null,
        authorDisplayName: mockAuthUser.displayName,
        workTitle: payload.workTitle ?? payload.levelName,
        workDescription: payload.workDescription ?? payload.summary,
        levelName: payload.levelName,
        summary: payload.summary,
        themeTags: payload.themeTags,
        coverImageSrc: payload.coverImageSrc ?? null,
        coverAssetId: payload.coverAssetId ?? null,
        publicationStatus: 'draft',
        updatedAt: '2026-05-12T10:00:00.000Z',
        publishedAt: null,
        playCount: 0,
        remixCount: 0,
        likeCount: 0,
        publishReady: false,
        levels: payload.levels,
        anchorPack: buildPuzzleAnchorPack(),
      },
    }),
  );
  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [],
  });
  vi.mocked(generatePuzzleOnboardingWork).mockResolvedValue({
    item: {
      workId: 'onboarding-work-1',
      profileId: 'onboarding-profile-1',
      ownerUserId: 'onboarding-guest',
      sourceSessionId: null,
      authorDisplayName: '陶泥儿主',
      workTitle: '梦境拼图',
      workDescription: '我想飞上天',
      levelName: '云上飞行',
      summary: '我想飞上天',
      themeTags: ['新手引导', '拼图'],
      coverImageSrc: 'data:image/svg+xml;utf8,onboarding',
      coverAssetId: 'onboarding-asset-1',
      publicationStatus: 'draft',
      updatedAt: '2026-05-05T12:00:00.000Z',
      publishedAt: null,
      playCount: 0,
      remixCount: 0,
      likeCount: 0,
      publishReady: true,
      levels: [],
    },
    level: {
      levelId: 'onboarding-level-1',
      levelName: '云上飞行',
      pictureDescription: '我想飞上天',
      pictureReference: null,
      candidates: [
        {
          candidateId: 'onboarding-candidate-1',
          imageSrc: 'data:image/svg+xml;utf8,onboarding',
          assetId: 'onboarding-asset-1',
          prompt: '我想飞上天',
          actualPrompt: '我想飞上天',
          sourceType: 'generated',
          selected: true,
        },
      ],
      selectedCandidateId: 'onboarding-candidate-1',
      coverImageSrc: 'data:image/svg+xml;utf8,onboarding',
      coverAssetId: 'onboarding-asset-1',
      generationStatus: 'ready',
    },
  });
  vi.mocked(savePuzzleOnboardingWork).mockResolvedValue({
    item: {
      workId: 'onboarding-work-saved',
      profileId: 'onboarding-profile-saved',
      ownerUserId: mockAuthUser.id,
      sourceSessionId: 'puzzle-session-onboarding',
      authorDisplayName: mockAuthUser.displayName,
      workTitle: '梦境拼图',
      workDescription: '我想飞上天',
      levelName: '云上飞行',
      summary: '我想飞上天',
      themeTags: ['新手引导', '拼图'],
      coverImageSrc: 'data:image/svg+xml;utf8,onboarding',
      coverAssetId: 'onboarding-asset-1',
      publicationStatus: 'draft',
      updatedAt: '2026-05-05T12:00:00.000Z',
      publishedAt: null,
      playCount: 0,
      remixCount: 0,
      likeCount: 0,
      publishReady: true,
      levels: [],
      anchorPack: {
        themePromise: {
          key: 'theme_promise',
          label: '主题承诺',
          value: '新手引导',
          status: 'confirmed',
        },
        visualSubject: {
          key: 'visual_subject',
          label: '视觉主体',
          value: '云上飞行',
          status: 'confirmed',
        },
        visualMood: {
          key: 'visual_mood',
          label: '视觉气质',
          value: '明亮',
          status: 'confirmed',
        },
        compositionHooks: {
          key: 'composition_hooks',
          label: '构图钩子',
          value: '天空',
          status: 'confirmed',
        },
        tagsAndForbidden: {
          key: 'tags_and_forbidden',
          label: '标签与禁区',
          value: '拼图',
          status: 'confirmed',
        },
      },
    },
  });
  vi.mocked(remixPuzzleGalleryWork).mockRejectedValue(
    new Error('未启用拼图 remix'),
  );
  vi.mocked(startPuzzleRun).mockImplementation(async (payload) => {
    const run = buildMockPuzzleRun(payload.profileId, '后端拼图关卡');
    return {
      run: {
        ...run,
        currentLevel: run.currentLevel
          ? {
              ...run.currentLevel,
              levelId: payload.levelId ?? run.currentLevel.levelId,
              startedAtMs: Date.now(),
            }
          : run.currentLevel,
      },
    };
  });
  vi.mocked(advancePuzzleNextLevel).mockImplementation(async (runId) => ({
    run: buildMockPuzzleRun(`${runId}-next-profile`, '后端推荐下一关'),
  }));
  vi.mocked(getPuzzleRun).mockImplementation(async (runId) => ({
    run: buildMockPuzzleRun(`${runId}-profile`, '后端同步关卡'),
  }));
  vi.mocked(updatePuzzleRunPause).mockImplementation(async (runId) => ({
    run: buildMockPuzzleRun(`${runId}-profile`, '后端同步关卡'),
  }));
  vi.mocked(usePuzzleRuntimeProp).mockImplementation(async (runId) => ({
    run: buildMockPuzzleRun(`${runId}-profile`, '后端同步关卡'),
  }));
  vi.mocked(startLocalPuzzleRun).mockImplementation((item, levelId) => {
    const runId = `local-puzzle-run-${item.profileId}`;
    const firstLevel = item.levels?.[0] ?? null;
    return {
      ...buildMockPuzzleRun(
        item.profileId,
        firstLevel?.levelName ?? item.levelName,
      ),
      runId,
      entryProfileId: item.profileId,
      currentLevel: {
        ...buildMockPuzzleRun(
          item.profileId,
          firstLevel?.levelName ?? item.levelName,
        ).currentLevel!,
        runId,
        levelId: levelId ?? firstLevel?.levelId ?? null,
        coverImageSrc: firstLevel?.coverImageSrc ?? item.coverImageSrc,
        uiBackgroundImageSrc:
          firstLevel?.uiBackgroundImageSrc ??
          (firstLevel?.uiBackgroundImageObjectKey
            ? `/${firstLevel.uiBackgroundImageObjectKey.replace(/^\/+/u, '')}`
            : null),
        uiBackgroundImageObjectKey:
          firstLevel?.uiBackgroundImageObjectKey ?? null,
        backgroundMusic: firstLevel?.backgroundMusic ?? null,
      },
    };
  });
  vi.mocked(submitPuzzleLeaderboard).mockImplementation(
    async (runId, payload) => ({
      run: {
        ...buildMockPuzzleRun(payload.profileId, '服务端排行榜快照'),
        runId,
        entryProfileId: payload.profileId,
        leaderboardEntries: [
          {
            rank: 1,
            nickname: payload.nickname,
            elapsedMs: payload.elapsedMs,
            isCurrentPlayer: true,
          },
        ],
        currentLevel: {
          ...buildMockPuzzleRun(payload.profileId, '服务端排行榜快照')
            .currentLevel!,
          runId,
          profileId: payload.profileId,
          gridSize: payload.gridSize,
          leaderboardEntries: [
            {
              rank: 1,
              nickname: payload.nickname,
              elapsedMs: payload.elapsedMs,
              isCurrentPlayer: true,
            },
          ],
        },
      },
    }),
  );
  vi.mocked(dragLocalPuzzlePiece).mockImplementation((run) => run);
  vi.mocked(swapLocalPuzzlePieces).mockImplementation((run) => run);
  vi.mocked(executeRpgCreationAction).mockResolvedValue({
    operation: {
      operationId: 'operation-draft-foundation-1',
      type: 'draft_foundation',
      status: 'queued',
      phaseLabel: '已接收请求',
      phaseDetail: '正在准备生成世界底稿。',
      progress: 10,
      error: null,
    },
  });
  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'running',
    phaseLabel: '生成世界底稿',
    phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
    progress: 38,
    error: null,
  });
  vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
  vi.mocked(streamRpgCreationMessage).mockResolvedValue(mockSession);
  vi.mocked(createCreativeAgentSession).mockResolvedValue({
    session: buildMockCreativeAgentSession(),
  });
  vi.mocked(streamCreativeAgentMessage).mockImplementation(
    async (sessionId, payload) =>
      buildMockCreativeAgentSession({
        sessionId,
        stage: 'collaborating',
        messages: [
          {
            id: 'creative-agent-message-1',
            role: 'assistant',
            kind: 'chat',
            text: '说一个灵感，我来帮你做成互动内容。',
            createdAt: '2026-05-05T10:00:00.000Z',
          },
          {
            id: payload.clientMessageId,
            role: 'user',
            kind: 'chat',
            text: payload.content
              .map((part) =>
                part.type === 'input_text' ? part.text.trim() : '参考图',
              )
              .filter(Boolean)
              .join(' / '),
            createdAt: '2026-05-05T10:01:00.000Z',
          },
          {
            id: 'creative-agent-message-2',
            role: 'assistant',
            kind: 'chat',
            text: '收到，我先帮你整理成可创作方案。',
            createdAt: '2026-05-05T10:01:01.000Z',
          },
        ],
      }),
  );
  vi.mocked(cancelCreativeAgentSession).mockResolvedValue({
    session: buildMockCreativeAgentSession({ stage: 'failed' }),
  });
  vi.mocked(confirmCreativePuzzleTemplate).mockResolvedValue({
    session: buildMockCreativeAgentSession(),
  });
  vi.mocked(streamCreativeDraftEdit).mockResolvedValue(
    buildMockCreativeAgentSession(),
  );
});

afterEach(() => {
  vi.unstubAllEnvs();
});

test('create tab shows template tabs and embeds puzzle form by default', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  expect(screen.queryByText('后台拼图赛')).toBeNull();
  await openCreateTemplateHub(user);

  expect(screen.getByRole('tablist', { name: '创作入口页签' })).toBeTruthy();
  expect(await screen.findByText('后台拼图赛')).toBeTruthy();
  expect(screen.getByText('后台抓大鹅赛')).toBeTruthy();
  expect(
    screen.getByRole('tablist', { name: '创作入口页签' }).className,
  ).toContain('scroll-px-2');
  expect(
    screen.getByRole('tab', { name: '热门推荐' }).getAttribute('aria-selected'),
  ).toBe('true');
  expect(await findCreationTypeButton('拼图')).toBeTruthy();
  expect(await findCreationTypeButton('文字冒险')).toBeTruthy();
  expect(await findCreationTypeButton('抓大鹅')).toBeTruthy();
  expect(await findCreationTypeButton('汪汪声浪')).toBeTruthy();
  expect(await findCreationTypeButton('宝贝识物')).toBeTruthy();
  expect(queryCreationTypeButton('智能创作')).toBeNull();
  expect(screen.queryByRole('button', { name: /智能创作/u })).toBeNull();
  expect(screen.queryByPlaceholderText('问一问陶泥儿')).toBeNull();
  expect(screen.queryByRole('button', { name: /角色扮演/u })).toBeNull();
  expect(createRpgCreationSession).not.toHaveBeenCalled();
  expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
  expect(createPuzzleAgentSession).not.toHaveBeenCalled();
});

test('create tab shows recent template cards when backend returns failed drafts', async () => {
  const user = userEvent.setup();
  mockExistingRpgDraftShelf({
    title: '入口可见的失败草稿',
    summary: '失败草稿也要进入创作入口最近创作。',
    stage: 'failed',
    stageLabel: '生成失败待处理',
    updatedAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
  });

  render(<TestWrapper withAuth />);
  await clickFirstButtonByName(user, '创作');
  const panel = getPlatformTabPanel('create');

  const tablist = await within(panel).findByRole('tablist', {
    name: '创作入口页签',
  });
  expect(tablist).toBeTruthy();
  expect(
    within(panel)
      .getByRole('tab', { name: '最近创作' })
      .getAttribute('aria-selected'),
  ).toBe('true');
  expect(
    await within(panel).findByRole('button', { name: /文字冒险/u }),
  ).toBeTruthy();
  expect(within(panel).getByText('仅显示最近7天内使用过的模板')).toBeTruthy();
  expect(within(panel).getByText('经典 RPG 体验')).toBeTruthy();
  expect(within(panel).queryByText('入口可见的失败草稿')).toBeNull();
  expect(
    within(panel).queryByText('失败草稿也要进入创作入口最近创作。'),
  ).toBeNull();
  expect(within(panel).queryByText('生成失败待处理')).toBeNull();
});

test('create tab refreshes recent works after opening from an empty draft shelf', async () => {
  const user = userEvent.setup();
  const failedDraft = buildExistingRpgDraftWork({
    title: '点击创作后出现的失败草稿',
    summary: '创作入口需要在进入时重新读取真实作品架。',
    stage: 'error',
    stageLabel: '发生错误',
    updatedAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
  });
  vi.mocked(listRpgCreationWorks)
    .mockResolvedValueOnce([])
    .mockResolvedValue([failedDraft]);

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  expect(
    within(getPlatformTabPanel('saves')).getByText('还没有作品'),
  ).toBeTruthy();

  await clickFirstButtonByName(user, '创作');
  const panel = getPlatformTabPanel('create');

  expect(
    await within(panel).findByRole('button', { name: /文字冒险/u }),
  ).toBeTruthy();
  expect(within(panel).getByText('仅显示最近7天内使用过的模板')).toBeTruthy();
  expect(within(panel).getByText('经典 RPG 体验')).toBeTruthy();
  expect(within(panel).queryByText('点击创作后出现的失败草稿')).toBeNull();
  expect(
    within(panel).queryByText('创作入口需要在进入时重新读取真实作品架。'),
  ).toBeNull();
  expect(within(panel).queryByText('发生错误')).toBeNull();
  await waitFor(() => {
    expect(
      vi.mocked(listRpgCreationWorks).mock.calls.length,
    ).toBeGreaterThanOrEqual(2);
  });
});

test('create tab opens match3d entry form from the template card', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));

  expect(await screen.findByText('抓大鹅工作区：missing-session')).toBeTruthy();
  expect(createPuzzleAgentSession).not.toHaveBeenCalled();
  expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
});

test('create tab opens puzzle entry form from the template card', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await openPuzzleFormFromCreateHub(user);

  expect(await screen.findByText('拼图工作区：missing-session')).toBeTruthy();
  expect(createPuzzleAgentSession).not.toHaveBeenCalled();
});

test('create tab opens bark battle entry form from the template card', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('汪汪声浪'));

  expect(await screen.findByText('汪汪声浪配置表单')).toBeTruthy();
  expect(screen.queryByText('汪汪声浪运行态')).toBeNull();
  expect(createBarkBattleDraft).not.toHaveBeenCalled();
  expect(publishBarkBattleWork).not.toHaveBeenCalled();
});

test('bark battle draft result can test before publish and publish to work detail', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('汪汪声浪'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  expect(createBarkBattleDraft).toHaveBeenCalledWith({
    title: '汪汪测试杯',
    description: '',
    themeDescription: '阳光草坪声浪竞技场',
    playerImageDescription: '戴红色围巾的柯基选手',
    opponentImageDescription: '蓝色护目镜哈士奇对手',
    difficultyPreset: 'normal',
  });
  await waitFor(() => {
    expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith(
      expect.objectContaining({
        draftId: 'bark-battle-draft-1',
        workId: 'bark-battle-work-1',
        playerCharacterImageSrc: '/generated-bark-battle/player.png',
        opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
        uiBackgroundImageSrc: '/generated-bark-battle/background.png',
      }),
    );
  });
  expect(await screen.findByText(/汪汪声浪结果页：汪汪测试杯/u)).toBeTruthy();
  expect(await screen.findByText('作品ID：bark-battle-work-1')).toBeTruthy();
  expect(publishBarkBattleWork).not.toHaveBeenCalled();

  await user.click(screen.getByRole('button', { name: '试玩' }));
  expect(await screen.findByText(/汪汪声浪运行态：汪汪测试杯/u)).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回配置' }));

  expect(await screen.findByText(/汪汪声浪结果页：汪汪测试杯/u)).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '发布' }));

  expect(publishBarkBattleWork).toHaveBeenCalledWith({
    draftId: 'bark-battle-draft-1',
    workId: 'bark-battle-work-1',
    publishedSnapshot: expect.objectContaining({
      title: '汪汪测试杯',
      themeDescription: '阳光草坪声浪竞技场',
      playerImageDescription: '戴红色围巾的柯基选手',
      opponentImageDescription: '蓝色护目镜哈士奇对手',
    }),
  });
  await waitFor(() => {
    expect(window.location.pathname).toBe('/works/detail');
    expect(window.location.search).toBe('?work=BB-TLEWORK1');
  });
  expect(await screen.findByText('分享给朋友')).toBeTruthy();
  expect(screen.getByText(/作品号：BB-TLEWORK1/u)).toBeTruthy();
  expect(screen.queryByText(/汪汪声浪运行态：汪汪测试杯/u)).toBeNull();
});

test('direct bark battle runtime public code opens published runtime', async () => {
  const publicWork = buildMockBarkBattleWork();
  vi.mocked(listBarkBattleGallery).mockResolvedValueOnce({
    items: [publicWork],
  });
  window.history.replaceState(
    null,
    '',
    '/runtime/bark-battle?work=BB-C661A45F',
  );

  render(<TestWrapper withAuth />);

  expect(await screen.findByText(/汪汪声浪运行态：汪汪公开杯/u)).toBeTruthy();
  expect(screen.getByTestId('bark-battle-runtime-mode').textContent).toBe(
    'published',
  );
  expect(screen.getByTestId('bark-battle-runtime-work-id').textContent).toBe(
    'BB-C661A45F',
  );
  expect(screen.getByTestId('bark-battle-runtime-player-src').textContent).toBe(
    '/generated-bark-battle/player.png',
  );
  expect(screen.queryByText('分享给朋友')).toBeNull();
});

test('bark battle form checks mud points before creating image assets', async () => {
  const user = userEvent.setup();
  vi.mocked(getProfileDashboard).mockResolvedValue({
    walletBalance: 5,
    totalPlayTimeMs: 0,
    playedWorldCount: 0,
    updatedAt: '2026-05-14T10:00:00.000Z',
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('汪汪声浪'));
  const titleInput = await screen.findByLabelText('汪汪作品标题');
  await user.clear(titleInput);
  await user.type(titleInput, '自定义声浪杯');
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
  expect(
    within(noticeDialog).getByText('本次需要 6 泥点，当前 5 泥点。'),
  ).toBeTruthy();
  expect(screen.getByText('汪汪声浪配置表单')).toBeTruthy();
  expect(screen.queryByRole('tablist', { name: '创作入口页签' })).toBeNull();
  expect(
    (screen.getByLabelText('汪汪作品标题') as HTMLInputElement).value,
  ).toBe('自定义声浪杯');
  expect(createBarkBattleDraft).not.toHaveBeenCalled();
  expect(generateAllBarkBattleImageAssets).not.toHaveBeenCalled();
});

test('bark battle draft is visible in draft shelf while image assets are generating', async () => {
  const user = userEvent.setup();
  vi.mocked(generateAllBarkBattleImageAssets).mockImplementation(
    () =>
      new Promise<Awaited<ReturnType<typeof generateAllBarkBattleImageAssets>>>(
        () => undefined,
      ),
  );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('汪汪声浪'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  expect(
    await screen.findByRole('progressbar', {
      name: '汪汪声浪素材生成进度',
    }),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回编辑' }));
  await openDraftHub(user);

  const panel = getPlatformTabPanel('saves');
  expect(
    await within(panel).findByRole('button', {
      name: /继续创作《汪汪测试杯》/u,
    }),
  ).toBeTruthy();
  await expectDraftHubGeneratingBadgeCountAtLeast(1);
  expect(listBarkBattleWorks).toHaveBeenCalled();
});

test('published bark battle stays visible when refresh temporarily returns only the duplicate draft', async () => {
  const user = userEvent.setup();
  vi.mocked(listBarkBattleWorks).mockResolvedValueOnce({
    items: [
      {
        workId: 'bark-battle-work-1',
        draftId: 'bark-battle-draft-1',
        ownerUserId: 'user-1',
        authorDisplayName: '测试玩家',
        title: '汪汪测试杯',
        summary: '',
        themeDescription: '阳光草坪声浪竞技场',
        playerImageDescription: '戴红色围巾的柯基选手',
        opponentImageDescription: '蓝色护目镜哈士奇对手',
        playerCharacterImageSrc: '/generated-bark-battle/player.png',
        opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
        uiBackgroundImageSrc: '/generated-bark-battle/background.png',
        difficultyPreset: 'normal',
        status: 'draft',
        generationStatus: 'ready',
        publishReady: true,
        playCount: 0,
        updatedAt: '2026-05-14T10:01:00.000Z',
        publishedAt: null,
      },
    ],
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('汪汪声浪'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));
  await waitFor(() => {
    expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith(
      expect.objectContaining({
        draftId: 'bark-battle-draft-1',
        workId: 'bark-battle-work-1',
      }),
    );
  });
  expect(await screen.findByText(/汪汪声浪结果页：汪汪测试杯/u)).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '发布' }));
  await waitFor(() => {
    expect(window.location.pathname).toBe('/works/detail');
  });
  await user.click(await screen.findByRole('button', { name: '返回' }));

  await openDraftHub(user);
  const panel = getPlatformTabPanel('saves');
  await user.click(within(panel).getByRole('tab', { name: /已发布/u }));

  expect(await within(panel).findByText('汪汪测试杯')).toBeTruthy();
  expect(
    within(panel).getByRole('button', { name: /查看详情《汪汪测试杯》/u }),
  ).toBeTruthy();
});

test('running match3d form generation can return to draft tab and reopen progress', async () => {
  const user = userEvent.setup();
  const runningSession = buildMockMatch3DAgentSession({
    draft: null,
    stage: 'collecting_config',
  });
  let resolveCompile!: (value: {
    session: Match3DAgentSessionSnapshot;
  }) => void;
  vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
    session: runningSession,
  });
  vi.mocked(match3dCreationClient.executeAction).mockReturnValue(
    new Promise((resolve) => {
      resolveCompile = resolve;
    }),
  );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );

  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);

  expect((await screen.findAllByText('抓大鹅草稿')).length).toBeGreaterThan(0);
  await expectDraftHubGeneratingBadgeCountAtLeast(1);

  await user.click(
    screen.getByRole('button', { name: /继续创作《抓大鹅草稿》/u }),
  );
  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();

  await act(async () => {
    resolveCompile({ session: buildMockMatch3DAgentSession() });
  });
});

test('background match3d draft failure notifies and reopens failed retry page', async () => {
  const user = userEvent.setup();
  const runningSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-background-failed-session',
    draft: null,
    stage: 'collecting_config',
  });
  const persistedFailedWork: Match3DWorkSummary = {
    workId: 'match3d-background-failed-work',
    profileId: 'match3d-background-failed-profile',
    ownerUserId: 'user-1',
    sourceSessionId: runningSession.sessionId,
    gameName: '失败中的抓鹅',
    themeText: '泥塑水果摊',
    summary: '正在生成玩法素材。',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-18T12:05:00.000Z',
    publishedAt: null,
    publishReady: false,
    generationStatus: 'generating',
    generatedItemAssets: [],
  };
  let rejectCompile!: (reason?: unknown) => void;
  vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
    session: runningSession,
  });
  vi.mocked(match3dCreationClient.executeAction).mockReturnValue(
    new Promise((_, reject) => {
      rejectCompile = reject;
    }),
  );
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: buildMockMatch3DAgentSession({
      sessionId: runningSession.sessionId,
      stage: 'collecting_config',
      draft: null,
      updatedAt: '2026-05-18T12:05:00.000Z',
    }),
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );
  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  await expectDraftHubGeneratingBadgeCountAtLeast(1);
  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [persistedFailedWork],
  });

  await act(async () => {
    rejectCompile(new Error('抓大鹅素材服务失败'));
    await Promise.resolve();
  });

  const failureDialog = await screen.findByRole('dialog', {
    name: '发生错误',
  });
  expect(within(failureDialog).getByText(/抓大鹅素材服务失败/u)).toBeTruthy();
  await user.click(within(failureDialog).getByRole('button', { name: '关闭' }));

  const draftPanel = getPlatformTabPanel('saves');
  const reopenButton = await within(draftPanel).findByRole('button', {
    name: /继续创作《(?:失败中的抓鹅|抓大鹅草稿)》/u,
  });
  expect(within(draftPanel).getByText('赛博水果摊')).toBeTruthy();
  await user.click(reopenButton);

  expect(await screen.findByText(/生成失败/u)).toBeTruthy();
  const reopenedFailureDialog = await screen.findByRole('dialog', {
    name: '发生错误',
  });
  await user.click(
    within(reopenedFailureDialog).getByRole('button', { name: '关闭' }),
  );
  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '发生错误' })).toBeNull();
  });
  expect(
    await screen.findByRole('button', { name: '重新生成草稿' }),
  ).toBeTruthy();
  expect(match3dCreationClient.executeAction).toHaveBeenCalledTimes(1);
});

test('failed match3d draft retry reuses current session instead of creating another draft', async () => {
  const user = userEvent.setup();
  const failedSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-retry-failed-session',
    draft: null,
    stage: 'collecting_config',
    updatedAt: '2026-05-18T12:05:00.000Z',
  });
  const persistedFailedWork: Match3DWorkSummary = {
    workId: 'match3d-retry-failed-work',
    profileId: 'match3d-retry-failed-profile',
    ownerUserId: 'user-1',
    sourceSessionId: failedSession.sessionId,
    gameName: '重试抓鹅',
    themeText: '霓虹水果摊',
    summary: '抓大鹅素材生成失败，可重新打开处理。',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-18T12:05:00.000Z',
    publishedAt: null,
    publishReady: false,
    generationStatus: 'generating',
    generatedItemAssets: [],
  };
  let rejectCompile!: (reason?: unknown) => void;
  vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
    session: failedSession,
  });
  vi.mocked(match3dCreationClient.executeAction).mockReturnValueOnce(
    new Promise((_, reject) => {
      rejectCompile = reject;
    }),
  );
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: failedSession,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );
  await screen.findByRole('progressbar', { name: '抓大鹅草稿生成进度' });
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [persistedFailedWork],
  });

  await act(async () => {
    rejectCompile(new Error('抓大鹅素材服务失败'));
    await Promise.resolve();
  });
  const failureDialog = await screen.findByRole('dialog', {
    name: '发生错误',
  });
  await user.click(within(failureDialog).getByRole('button', { name: '关闭' }));

  const draftPanel = getPlatformTabPanel('saves');
  await user.click(
    await within(draftPanel).findByRole('button', {
      name: /继续创作《(?:重试抓鹅|抓大鹅草稿)》/u,
    }),
  );
  const reopenedFailureDialog = await screen.findByRole('dialog', {
    name: '发生错误',
  });
  await user.click(
    within(reopenedFailureDialog).getByRole('button', { name: '关闭' }),
  );
  vi.mocked(match3dCreationClient.executeAction).mockResolvedValueOnce({
    session: buildMockMatch3DAgentSession({
      sessionId: failedSession.sessionId,
      stage: 'draft_ready',
      draft: {
        profileId: persistedFailedWork.profileId,
        gameName: persistedFailedWork.gameName,
        themeText: persistedFailedWork.themeText,
        summary: persistedFailedWork.summary,
        tags: persistedFailedWork.tags,
        coverImageSrc: null,
        referenceImageSrc: null,
        clearCount: persistedFailedWork.clearCount,
        difficulty: persistedFailedWork.difficulty,
        generatedItemAssets: [],
      },
    }),
  });

  await user.click(await screen.findByRole('button', { name: '重新生成草稿' }));

  await waitFor(() => {
    expect(match3dCreationClient.executeAction).toHaveBeenCalledTimes(2);
  });
  expect(match3dCreationClient.createSession).toHaveBeenCalledTimes(1);
  expect(match3dCreationClient.executeAction).toHaveBeenNthCalledWith(
    2,
    failedSession.sessionId,
    expect.objectContaining({ action: 'match3d_compile_draft' }),
  );
});

test('running match3d persisted draft reopens progress instead of unfinished result', async () => {
  const user = userEvent.setup();
  const runningSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-running-persisted-session',
    draft: null,
    stage: 'collecting_config',
  });
  const persistedRunningSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-running-persisted-session',
    stage: 'draft_ready',
    draft: {
      profileId: 'match3d-running-persisted-profile',
      gameName: '赛博水果摊',
      themeText: '赛博水果摊',
      summary: '',
      tags: ['水果', '抓大鹅'],
      coverImageSrc: null,
      referenceImageSrc: null,
      clearCount: 12,
      difficulty: 4,
      generatedItemAssets: [],
    },
  });
  const persistedRunningWork: Match3DWorkSummary = {
    workId: 'match3d-running-persisted-work',
    profileId: 'match3d-running-persisted-profile',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-running-persisted-session',
    gameName: '赛博水果摊',
    themeText: '赛博水果摊',
    summary: '正在生成玩法素材。',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-14T10:30:00.000Z',
    publishedAt: null,
    publishReady: false,
    generatedItemAssets: [],
  };
  vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
    session: runningSession,
  });
  vi.mocked(match3dCreationClient.executeAction).mockRejectedValueOnce(
    new Error('素材生成仍在后台处理'),
  );
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: persistedRunningSession,
  });
  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [persistedRunningWork],
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: persistedRunningWork,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );
  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();
  expect(await screen.findAllByText('素材生成仍在后台处理')).not.toHaveLength(
    0,
  );
  vi.mocked(match3dCreationClient.getSession).mockClear();

  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  await expectDraftHubGeneratingBadgeCountAtLeast(1);

  await user.click(
    await screen.findByRole('button', { name: /继续创作《赛博水果摊》/u }),
  );

  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();
  expect(screen.queryByText('抓大鹅结果页')).toBeNull();
});

test('persisted generating match3d draft opens generation progress after refresh', async () => {
  const user = userEvent.setup();
  const persistedGeneratingWork: Match3DWorkSummary = {
    workId: 'match3d-work-generating',
    profileId: 'match3d-profile-generating',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-session-generating',
    gameName: '生成中抓鹅',
    themeText: '霓虹水果摊',
    summary: '刷新后仍应回到抓大鹅生成面板。',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-18T12:05:00.000Z',
    publishedAt: null,
    publishReady: false,
    generationStatus: 'generating',
    generatedItemAssets: [],
  };

  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [persistedGeneratingWork],
  });
  vi.mocked(match3dCreationClient.getSession).mockResolvedValueOnce({
    session: buildMockMatch3DAgentSession({
      sessionId: 'match3d-session-generating',
      draft: {
        profileId: 'match3d-profile-generating',
        gameName: '生成中抓鹅',
        themeText: '霓虹水果摊',
        summary: '刷新后仍应回到抓大鹅生成面板。',
        tags: ['水果', '抓大鹅'],
        coverImageSrc: null,
        referenceImageSrc: null,
        clearCount: 12,
        difficulty: 4,
        generatedItemAssets: [],
      },
      stage: 'draft_ready',
      lastAssistantReply: '正在生成抓大鹅素材。',
      updatedAt: '2026-05-18T12:05:00.000Z',
    }),
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(
    await screen.findByRole('button', { name: /继续创作《生成中抓鹅》/u }),
  );

  await waitFor(() => {
    expect(match3dCreationClient.getSession).toHaveBeenCalledWith(
      'match3d-session-generating',
    );
  });
  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();
  const restoredProgressValue = Number(
    screen
      .getByRole('progressbar', { name: '抓大鹅草稿生成进度' })
      .getAttribute('aria-valuenow'),
  );
  expect(restoredProgressValue).toBeGreaterThan(0);
  expect(restoredProgressValue).toBeLessThan(100);
  expect(screen.getByText(`${restoredProgressValue}%`)).toBeTruthy();
  expect(screen.queryByText('抓大鹅结果页')).toBeNull();
  expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
    'match3d-profile-generating',
  );
});

test('running match3d form generation keeps other creation templates available', async () => {
  const user = userEvent.setup();
  const runningSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-running-session',
    draft: null,
    stage: 'collecting_config',
  });
  let resolveCompile!: (value: {
    session: Match3DAgentSessionSnapshot;
  }) => void;
  vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
    session: runningSession,
  });
  vi.mocked(match3dCreationClient.executeAction).mockReturnValue(
    new Promise((resolve) => {
      resolveCompile = resolve;
    }),
  );
  const puzzleReadySession: PuzzleAgentSessionSnapshot = {
    sessionId: 'puzzle-session-parallel-1',
    seedText: '暖灯猫街',
    currentTurn: 1,
    progressPercent: 100,
    stage: 'ready_to_publish',
    anchorPack: buildPuzzleAnchorPack(),
    draft: {
      workTitle: '并行拼图',
      workDescription: '抓大鹅后台生成时创建的新拼图。',
      levelName: '并行拼图',
      summary: '抓大鹅后台生成时创建的新拼图。',
      themeTags: ['并行创作'],
      forbiddenDirectives: [],
      creatorIntent: null,
      anchorPack: buildPuzzleAnchorPack(),
      candidates: [
        {
          candidateId: 'candidate-parallel-1',
          imageSrc: '/puzzle/parallel-candidate.png',
          assetId: 'asset-parallel-1',
          prompt: '暖灯猫街',
          actualPrompt: null,
          sourceType: 'generated',
          selected: true,
        },
      ],
      selectedCandidateId: 'candidate-parallel-1',
      coverImageSrc: '/puzzle/parallel-candidate.png',
      coverAssetId: 'asset-parallel-1',
      generationStatus: 'ready',
      levels: [
        {
          levelId: 'puzzle-level-parallel-1',
          levelName: '并行拼图',
          pictureDescription: '一只猫在雨夜灯牌下回头。',
          pictureReference: null,
          candidates: [
            {
              candidateId: 'candidate-parallel-1',
              imageSrc: '/puzzle/parallel-candidate.png',
              assetId: 'asset-parallel-1',
              prompt: '暖灯猫街',
              actualPrompt: null,
              sourceType: 'generated',
              selected: true,
            },
          ],
          selectedCandidateId: 'candidate-parallel-1',
          coverImageSrc: '/puzzle/parallel-candidate.png',
          coverAssetId: 'asset-parallel-1',
          generationStatus: 'ready',
        },
      ],
    },
    messages: [],
    lastAssistantReply: '拼图草稿已经生成。',
    publishedProfileId: null,
    suggestedActions: [],
    resultPreview: null,
    updatedAt: '2026-05-13T10:00:00.000Z',
  };
  vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
    operation: {
      operationId: 'compile-puzzle-parallel-1',
      type: 'compile_puzzle_draft',
      status: 'completed',
      phaseLabel: '已完成',
      phaseDetail: '草稿已生成',
      progress: 1,
    },
    session: puzzleReadySession,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );
  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openCreateTemplateHub(user);
  const puzzleCard = await findCreationTypeButton('拼图');
  expect((puzzleCard as HTMLButtonElement).disabled).toBe(false);

  await user.click(puzzleCard);
  const generatePuzzleButton = await screen.findByRole('button', {
    name: '生成草稿',
  });
  expect((generatePuzzleButton as HTMLButtonElement).disabled).toBe(false);
  await user.click(generatePuzzleButton);

  await waitFor(() => {
    expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1);
  });
  expect(executePuzzleAgentAction).toHaveBeenCalledWith(
    'puzzle-session-1',
    expect.objectContaining({
      action: 'compile_puzzle_draft',
    }),
  );
  expect(match3dCreationClient.executeAction).toHaveBeenCalledTimes(1);

  await act(async () => {
    resolveCompile({ session: buildMockMatch3DAgentSession() });
  });
});

test('running match3d form generation keeps same template generation available', async () => {
  const user = userEvent.setup();
  const firstSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-parallel-session-1',
    draft: null,
    stage: 'collecting_config',
  });
  const secondSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-parallel-session-2',
    draft: null,
    stage: 'collecting_config',
  });
  let resolveFirstCompile!: (value: {
    session: Match3DAgentSessionSnapshot;
  }) => void;
  let resolveSecondCompile!: (value: {
    session: Match3DAgentSessionSnapshot;
  }) => void;
  vi.mocked(match3dCreationClient.createSession)
    .mockResolvedValueOnce({ session: firstSession })
    .mockResolvedValueOnce({ session: secondSession });
  vi.mocked(match3dCreationClient.executeAction)
    .mockReturnValueOnce(
      new Promise((resolve) => {
        resolveFirstCompile = resolve;
      }),
    )
    .mockReturnValueOnce(
      new Promise((resolve) => {
        resolveSecondCompile = resolve;
      }),
    );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );
  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openCreateTemplateHub(user);
  const match3dCard = await findCreationTypeButton('抓大鹅');
  expect((match3dCard as HTMLButtonElement).disabled).toBe(false);
  await user.click(match3dCard);

  const secondGenerateButton = await screen.findByRole('button', {
    name: '生成抓大鹅草稿',
  });
  expect((secondGenerateButton as HTMLButtonElement).disabled).toBe(false);
  expect(screen.getByTestId('match3d-workspace-busy-state')).toHaveProperty(
    'textContent',
    'idle',
  );
  await user.click(secondGenerateButton);

  await waitFor(() => {
    expect(match3dCreationClient.createSession).toHaveBeenCalledTimes(2);
  });
  expect(match3dCreationClient.executeAction).toHaveBeenCalledTimes(2);
  expect(match3dCreationClient.executeAction).toHaveBeenNthCalledWith(
    1,
    'match3d-parallel-session-1',
    expect.objectContaining({ action: 'match3d_compile_draft' }),
  );
  expect(match3dCreationClient.executeAction).toHaveBeenNthCalledWith(
    2,
    'match3d-parallel-session-2',
    expect.objectContaining({ action: 'match3d_compile_draft' }),
  );

  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  await waitFor(() => {
    expect(screen.getAllByText('抓大鹅草稿').length).toBeGreaterThanOrEqual(2);
  });
  await expectDraftHubGeneratingBadgeCountAtLeast(2);

  await act(async () => {
    resolveFirstCompile({
      session: buildMockMatch3DAgentSession({
        sessionId: 'match3d-parallel-session-1',
      }),
    });
    resolveSecondCompile({
      session: buildMockMatch3DAgentSession({
        sessionId: 'match3d-parallel-session-2',
      }),
    });
  });
});

test('running puzzle form generation creates a new puzzle draft on same template submit', async () => {
  const user = userEvent.setup();
  const firstSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-parallel-session-1',
  });
  const secondSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-parallel-session-2',
  });
  let resolveFirstCompile!: (value: {
    operation: {
      operationId: string;
      type: 'compile_puzzle_draft';
      status: 'completed';
      phaseLabel: string;
      phaseDetail: string;
      progress: number;
    };
    session: PuzzleAgentSessionSnapshot;
  }) => void;
  let resolveSecondCompile!: (value: {
    operation: {
      operationId: string;
      type: 'compile_puzzle_draft';
      status: 'completed';
      phaseLabel: string;
      phaseDetail: string;
      progress: number;
    };
    session: PuzzleAgentSessionSnapshot;
  }) => void;
  vi.mocked(createPuzzleAgentSession)
    .mockResolvedValueOnce({ session: firstSession })
    .mockResolvedValueOnce({ session: secondSession });
  vi.mocked(executePuzzleAgentAction)
    .mockReturnValueOnce(
      new Promise((resolve) => {
        resolveFirstCompile = resolve;
      }),
    )
    .mockReturnValueOnce(
      new Promise((resolve) => {
        resolveSecondCompile = resolve;
      }),
    );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));
  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openCreateTemplateHub(user);
  const puzzleCard = await findCreationTypeButton('拼图');
  expect((puzzleCard as HTMLButtonElement).disabled).toBe(false);
  await user.click(puzzleCard);

  expect(await screen.findByText(/拼图工作区：/u)).toBeTruthy();
  expect(screen.getByTestId('puzzle-workspace-busy-state')).toHaveProperty(
    'textContent',
    'idle',
  );
  const secondGenerateButton = await screen.findByRole('button', {
    name: '生成草稿',
  });
  expect((secondGenerateButton as HTMLButtonElement).disabled).toBe(false);
  await user.click(secondGenerateButton);

  await waitFor(() => {
    expect(createPuzzleAgentSession).toHaveBeenCalledTimes(2);
  });
  expect(executePuzzleAgentAction).toHaveBeenCalledTimes(2);
  expect(executePuzzleAgentAction).toHaveBeenNthCalledWith(
    1,
    'puzzle-parallel-session-1',
    expect.objectContaining({ action: 'compile_puzzle_draft' }),
  );
  expect(executePuzzleAgentAction).toHaveBeenNthCalledWith(
    2,
    'puzzle-parallel-session-2',
    expect.objectContaining({ action: 'compile_puzzle_draft' }),
  );

  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('saves')).getAllByRole('button', {
        name: /继续创作《[^》]+》，生成中/u,
      }).length,
    ).toBeGreaterThanOrEqual(2);
  });
  await expectDraftHubGeneratingBadgeCountAtLeast(2);

  await act(async () => {
    resolveFirstCompile({
      operation: {
        operationId: 'compile-puzzle-parallel-1',
        type: 'compile_puzzle_draft',
        status: 'completed',
        phaseLabel: '已完成',
        phaseDetail: '草稿已生成',
        progress: 1,
      },
      session: buildMockPuzzleAgentSession({
        sessionId: 'puzzle-parallel-session-1',
      }),
    });
    resolveSecondCompile({
      operation: {
        operationId: 'compile-puzzle-parallel-2',
        type: 'compile_puzzle_draft',
        status: 'completed',
        phaseLabel: '已完成',
        phaseDetail: '草稿已生成',
        progress: 1,
      },
      session: buildMockPuzzleAgentSession({
        sessionId: 'puzzle-parallel-session-2',
      }),
    });
  });
});

test('queued puzzle form generation stays on generation progress', async () => {
  const user = userEvent.setup();
  const queuedSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-queued-session-1',
    progressPercent: 5,
    lastAssistantReply: '拼图生成任务已进入后台队列。',
  });
  vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
    session: queuedSession,
  });
  vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
    operation: {
      operationId: 'compile-puzzle-queued-1',
      type: 'compile_puzzle_draft',
      status: 'queued',
      phaseLabel: '已进入后台队列',
      phaseDetail: '拼图草稿生成已进入后台队列。',
      progress: 5,
      error: null,
    },
    session: queuedSession,
  });
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: {
      ...queuedSession,
      progressPercent: 12,
    },
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  const progressbar = await screen.findByRole('progressbar', {
    name: '拼图图片生成进度',
  });
  expect(progressbar).toBeTruthy();
  expect(updatePuzzleWork).not.toHaveBeenCalled();
  expect(startLocalPuzzleRun).not.toHaveBeenCalled();
  expect(screen.queryByText('拼图结果页')).toBeNull();
  expect(window.location.pathname).not.toBe('/runtime/puzzle');
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('saves')).getAllByRole('button', {
        name: /继续创作《[^》]+》，生成中/u,
      }).length,
    ).toBeGreaterThanOrEqual(1);
  });
});

test('failed parallel puzzle generations stay as separate non-generating drafts', async () => {
  const user = userEvent.setup();
  const firstSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-parallel-failed-session-1',
  });
  const secondSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-parallel-failed-session-2',
  });
  let rejectFirstCompile!: (reason?: unknown) => void;
  let rejectSecondCompile!: (reason?: unknown) => void;
  vi.mocked(createPuzzleAgentSession)
    .mockResolvedValueOnce({
      session: firstSession,
    })
    .mockResolvedValueOnce({
      session: secondSession,
    });
  vi.mocked(executePuzzleAgentAction)
    .mockReturnValueOnce(
      new Promise((_, reject) => {
        rejectFirstCompile = reject;
      }),
    )
    .mockReturnValueOnce(
      new Promise((_, reject) => {
        rejectSecondCompile = reject;
      }),
    );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));
  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  expect(await screen.findByText('18泥点')).toBeTruthy();

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));
  await waitFor(() => {
    expect(createPuzzleAgentSession).toHaveBeenCalledTimes(2);
  });
  expect(executePuzzleAgentAction).toHaveBeenCalledTimes(2);

  await clickFirstButtonByName(user, '返回');
  expect(await screen.findByText('16泥点')).toBeTruthy();
  await openDraftHub(user);
  const draftPanel = getPlatformTabPanel('saves');
  await waitFor(() => {
    expect(
      within(draftPanel).getAllByRole('button', {
        name: /继续创作《[^》]+》，生成中/u,
      }).length,
    ).toBeGreaterThanOrEqual(2);
  });
  await expectDraftHubGeneratingBadgeCountAtLeast(2);

  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [
      {
        workId: `puzzle-work-${firstSession.sessionId}`,
        profileId: `puzzle-profile-${firstSession.sessionId}`,
        ownerUserId: 'user-1',
        sourceSessionId: firstSession.sessionId,
        authorDisplayName: '测试玩家',
        workTitle: '',
        workDescription: '一套雨夜猫街主题拼图。',
        levelName: '第1关',
        summary: '一套雨夜猫街主题拼图。',
        themeTags: [],
        coverImageSrc: null,
        coverAssetId: null,
        publicationStatus: 'draft',
        updatedAt: '2026-05-18T12:00:00.000Z',
        publishedAt: null,
        playCount: 0,
        remixCount: 0,
        likeCount: 0,
        publishReady: false,
        generationStatus: 'failed',
        levels: [],
      },
      {
        workId: `puzzle-work-${secondSession.sessionId}`,
        profileId: `puzzle-profile-${secondSession.sessionId}`,
        ownerUserId: 'user-1',
        sourceSessionId: secondSession.sessionId,
        authorDisplayName: '测试玩家',
        workTitle: '',
        workDescription: '一套雨夜猫街主题拼图。',
        levelName: '第1关',
        summary: '一套雨夜猫街主题拼图。',
        themeTags: [],
        coverImageSrc: null,
        coverAssetId: null,
        publicationStatus: 'draft',
        updatedAt: '2026-05-18T12:00:01.000Z',
        publishedAt: null,
        playCount: 0,
        remixCount: 0,
        likeCount: 0,
        publishReady: false,
        generationStatus: 'failed',
        levels: [],
      },
    ],
  });

  await act(async () => {
    rejectFirstCompile(
      new Error(
        '拼图 VectorEngine 图片编辑失败：创建图片编辑任务失败：error sending request for url (https://api.vectorengine.cn/v1/images/edits)',
      ),
    );
    rejectSecondCompile(
      new Error(
        '拼图 VectorEngine 图片编辑失败：创建图片编辑任务失败：error sending request for url (https://api.vectorengine.cn/v1/images/edits)',
      ),
    );
    await Promise.resolve();
  });

  await waitFor(() => {
    expect(
      within(draftPanel).getAllByRole('button', {
        name: /继续创作《[^》]+》/u,
      }).length,
    ).toBeGreaterThanOrEqual(2);
    expect(within(draftPanel).queryAllByLabelText('生成中')).toHaveLength(0);
  });
  expect(await screen.findByText('20泥点')).toBeTruthy();
  expect(within(draftPanel).queryByText('第1关')).toBeNull();
  expect(
    within(draftPanel).getAllByText('拼图草稿生成失败，可重新打开处理。')
      .length,
  ).toBeGreaterThanOrEqual(2);
  expect(
    within(draftPanel).getAllByText('一套雨夜猫街主题拼图。').length,
  ).toBeGreaterThanOrEqual(2);
  const failureDialog = await screen.findByRole('dialog', {
    name: '发生错误',
  });
  expect(
    within(failureDialog).getByText(/拼图 VectorEngine 图片编辑失败/u),
  ).toBeTruthy();
});

test('failed puzzle draft retry reuses current session instead of creating another draft', async () => {
  const user = userEvent.setup();
  const failedSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-retry-failed-session',
    draft: null,
    stage: 'collecting_anchors',
    updatedAt: '2026-05-18T12:00:00.000Z',
  });
  const persistedFailedWork: PuzzleWorkSummary = {
    workId: `puzzle-work-${failedSession.sessionId}`,
    profileId: `puzzle-profile-${failedSession.sessionId}`,
    ownerUserId: 'user-1',
    sourceSessionId: failedSession.sessionId,
    authorDisplayName: '测试玩家',
    workTitle: '',
    workDescription: '一套雨夜猫街主题拼图。',
    levelName: '第1关',
    summary: '一套雨夜猫街主题拼图。',
    themeTags: [],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'draft',
    updatedAt: '2026-05-18T12:00:00.000Z',
    publishedAt: null,
    playCount: 0,
    remixCount: 0,
    likeCount: 0,
    publishReady: false,
    generationStatus: 'failed',
    levels: [],
  };
  let rejectCompile!: (reason?: unknown) => void;
  vi.mocked(createPuzzleAgentSession).mockResolvedValue({
    session: failedSession,
  });
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: failedSession,
  });
  vi.mocked(executePuzzleAgentAction).mockReturnValueOnce(
    new Promise((_, reject) => {
      rejectCompile = reject;
    }),
  );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));
  await screen.findByRole('progressbar', { name: '拼图图片生成进度' });
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [persistedFailedWork],
  });

  await act(async () => {
    rejectCompile(new Error('拼图图片生成失败'));
    await Promise.resolve();
  });
  const failureDialog = await screen.findByRole('dialog', {
    name: '发生错误',
  });
  await user.click(within(failureDialog).getByRole('button', { name: '关闭' }));

  const draftPanel = getPlatformTabPanel('saves');
  await user.click(
    await within(draftPanel).findByRole('button', {
      name: /继续创作《[^》]+》/u,
    }),
  );
  const reopenedFailureDialog = await screen.findByRole('dialog', {
    name: '发生错误',
  });
  await user.click(
    within(reopenedFailureDialog).getByRole('button', { name: '关闭' }),
  );
  vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
    operation: {
      operationId: 'compile-puzzle-retry',
      type: 'compile_puzzle_draft',
      status: 'completed',
      phaseLabel: '已完成',
      phaseDetail: '草稿已生成',
      progress: 1,
    },
    session: buildMockPuzzleAgentSession({
      sessionId: failedSession.sessionId,
      stage: 'ready_to_publish',
      progressPercent: 100,
      draft: buildReadyPuzzleDraft(),
    }),
  });

  await user.click(await screen.findByRole('button', { name: '重新生成图片' }));

  await waitFor(() => {
    expect(executePuzzleAgentAction).toHaveBeenCalledTimes(2);
  });
  expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1);
  expect(executePuzzleAgentAction).toHaveBeenNthCalledWith(
    2,
    failedSession.sessionId,
    expect.objectContaining({ action: 'compile_puzzle_draft' }),
  );
});

test('running puzzle draft opens generation progress from draft tab', async () => {
  const user = userEvent.setup();
  const runningSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-running-session',
    draft: null,
    stage: 'collecting_anchors',
    progressPercent: 20,
  });
  let resolveCompile!: (value: {
    operation: {
      operationId: string;
      type: 'compile_puzzle_draft';
      status: 'completed';
      phaseLabel: string;
      phaseDetail: string;
      progress: number;
    };
    session: PuzzleAgentSessionSnapshot;
  }) => void;
  vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
    session: runningSession,
  });
  vi.mocked(executePuzzleAgentAction).mockReturnValueOnce(
    new Promise((resolve) => {
      resolveCompile = resolve;
    }),
  );
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: runningSession,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));
  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  await expectDraftHubGeneratingBadgeCountAtLeast(1);

  await user.click(
    screen.getByRole('button', { name: /继续创作《[^》]+》，生成中/u }),
  );

  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();
  expect(screen.queryByText('拼图结果页')).toBeNull();

  await act(async () => {
    resolveCompile({
      operation: {
        operationId: 'compile-puzzle-running',
        type: 'compile_puzzle_draft',
        status: 'completed',
        phaseLabel: '已完成',
        phaseDetail: '草稿已生成',
        progress: 1,
      },
      session: buildMockPuzzleAgentSession({
        sessionId: 'puzzle-running-session',
      }),
    });
  });
});

test('puzzle text-only form stays generating when compile starts background image without cover', async () => {
  const user = userEvent.setup();
  const initialSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-session-text-only',
    stage: 'collecting_anchors',
    progressPercent: 0,
    draft: null,
  });
  const generatingDraft = buildReadyPuzzleDraft({
    workTitle: '文字直创拼图',
    workDescription: '只输入文字后后台继续生成图片。',
    candidates: [],
    selectedCandidateId: null,
    coverImageSrc: null,
    coverAssetId: null,
    generationStatus: 'generating',
    levels: [
      {
        ...buildReadyPuzzleDraft().levels![0]!,
        candidates: [],
        selectedCandidateId: null,
        coverImageSrc: null,
        coverAssetId: null,
        levelSceneImageSrc: null,
        levelSceneImageObjectKey: null,
        uiSpritesheetImageSrc: null,
        uiSpritesheetImageObjectKey: null,
        levelBackgroundImageSrc: null,
        levelBackgroundImageObjectKey: null,
        generationStatus: 'generating',
      },
    ],
  });
  const generatingSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-session-text-only',
    stage: 'image_refining',
    progressPercent: 88,
    draft: generatingDraft,
    lastAssistantReply: '已编译首关草稿，并启动首关画面和 UI 资产后台生成。',
    resultPreview: {
      draft: generatingDraft,
      blockers: [
        {
          id: 'missing-cover-image-puzzle-level-1',
          code: 'MISSING_COVER_IMAGE',
          message: '正式拼图图片尚未确定',
        },
      ],
      qualityFindings: [],
      publishReady: false,
    },
  });

  vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
    session: initialSession,
  });
  vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
    operation: {
      operationId: 'compile-puzzle-text-only',
      type: 'compile_puzzle_draft',
      status: 'completed',
      phaseLabel: '首关拼图草稿',
      phaseDetail: '已编译首关草稿，并启动首关画面和 UI 资产后台生成。',
      progress: 0.88,
    },
    session: generatingSession,
  });
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: generatingSession,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();
  await waitFor(() => {
    expect(executePuzzleAgentAction).toHaveBeenCalledWith(
      'puzzle-session-text-only',
      expect.objectContaining({ action: 'compile_puzzle_draft' }),
    );
  });
  expect(screen.queryByRole('dialog', { name: '生成完成' })).toBeNull();
  expect(screen.queryByText('请先选择一张正式拼图图片。')).toBeNull();
  expect(screen.queryByText('拼图结果页')).toBeNull();
  expect(updatePuzzleWork).not.toHaveBeenCalled();
  expect(startLocalPuzzleRun).not.toHaveBeenCalled();
});

test('puzzle form checks mud points before creating a draft', async () => {
  const user = userEvent.setup();
  vi.mocked(getProfileDashboard).mockResolvedValue({
    walletBalance: 7,
    totalPlayTimeMs: 0,
    playedWorldCount: 0,
    updatedAt: '2026-05-14T10:00:00.000Z',
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
  expect(
    within(noticeDialog).getByText('本次需要 8 泥点，当前 7 泥点。'),
  ).toBeTruthy();
  expect(screen.getByText('拼图工作区：missing-session')).toBeTruthy();
  expect(screen.queryByRole('tablist', { name: '创作入口页签' })).toBeNull();
  expect(createPuzzleAgentSession).not.toHaveBeenCalled();
  expect(executePuzzleAgentAction).not.toHaveBeenCalled();
});

test('match3d form checks mud points before creating a draft', async () => {
  const user = userEvent.setup();
  vi.mocked(getProfileDashboard).mockResolvedValue({
    walletBalance: 11,
    totalPlayTimeMs: 0,
    playedWorldCount: 0,
    updatedAt: '2026-05-14T10:00:00.000Z',
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );

  const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
  expect(
    within(noticeDialog).getByText('本次需要 12 泥点，当前 11 泥点。'),
  ).toBeTruthy();
  expect(screen.getByText('抓大鹅工作区：missing-session')).toBeTruthy();
  expect(screen.queryByRole('tablist', { name: '创作入口页签' })).toBeNull();
  expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
  expect(match3dCreationClient.executeAction).not.toHaveBeenCalled();
});

test('match3d result trial passes generated models into first runtime mount', async () => {
  const user = userEvent.setup();
  const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
    {
      itemId: 'match3d-item-1',
      itemName: '草莓',
      imageSrc:
        '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
      imageObjectKey:
        'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
      modelSrc: null,
      modelObjectKey:
        'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
      modelFileName: 'strawberry.glb',
      taskUuid: 'task-strawberry',
      subscriptionKey: 'sub-strawberry',
      status: 'model_ready',
      error: null,
      backgroundAsset: match3DGeneratedUiAsset,
    },
  ];
  const match3dDraftWork: Match3DWorkSummary = {
    workId: 'match3d-work-draft-1',
    profileId: 'match3d-profile-draft-1',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-session-draft-1',
    gameName: '水果抓大鹅',
    themeText: '水果',
    summary: '',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-01T10:30:00.000Z',
    publishedAt: null,
    publishReady: false,
    generatedItemAssets,
  };
  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [match3dDraftWork],
  });
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: buildMockMatch3DAgentSession({
      sessionId: 'match3d-session-draft-1',
      draft: {
        profileId: 'match3d-profile-draft-1',
        gameName: '水果抓大鹅',
        themeText: '水果',
        summary: '',
        tags: ['水果', '抓大鹅'],
        coverImageSrc: null,
        referenceImageSrc: null,
        clearCount: 12,
        difficulty: 4,
        generatedItemAssets,
      },
    }),
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: match3dDraftWork,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dDraftWork.profileId),
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(
    await screen.findByRole('button', { name: /继续创作《水果抓大鹅》/u }),
  );
  expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '试玩' }));

  await waitFor(() => {
    expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
      'match3d-profile-draft-1',
      ISOLATED_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(
    await screen.findByTestId('match3d-runtime-generated-model-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    screen.getByTestId('match3d-runtime-generated-asset-count'),
  ).toHaveProperty('textContent', '1');
});

test('match3d result trial passes generated 2D image views into first runtime mount', async () => {
  const user = userEvent.setup();
  const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
    {
      itemId: 'match3d-item-1',
      itemName: '草莓',
      imageSrc: null,
      imageObjectKey: null,
      imageViews: [1, 2, 3, 4, 5].map((viewIndex) => ({
        viewId: `view-${String(viewIndex).padStart(2, '0')}`,
        viewIndex,
        imageSrc: `/generated-match3d-assets/session/profile/items/match3d-item-1-item/views/view-${String(viewIndex).padStart(2, '0')}.png`,
        imageObjectKey: null,
      })),
      modelSrc: null,
      modelObjectKey: null,
      modelFileName: null,
      taskUuid: null,
      subscriptionKey: null,
      status: 'image_ready',
      error: null,
    },
  ];
  const match3dDraftWork: Match3DWorkSummary = {
    workId: 'match3d-work-draft-2d-1',
    profileId: 'match3d-profile-draft-2d-1',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-session-draft-2d-1',
    gameName: '水果抓大鹅',
    themeText: '水果',
    summary: '',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-13T10:30:00.000Z',
    publishedAt: null,
    publishReady: false,
    generatedItemAssets,
  };
  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [match3dDraftWork],
  });
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: buildMockMatch3DAgentSession({
      sessionId: 'match3d-session-draft-2d-1',
      draft: {
        profileId: 'match3d-profile-draft-2d-1',
        gameName: '水果抓大鹅',
        themeText: '水果',
        summary: '',
        tags: ['水果', '抓大鹅'],
        coverImageSrc: null,
        referenceImageSrc: null,
        clearCount: 12,
        difficulty: 4,
        generatedItemAssets,
      },
    }),
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: match3dDraftWork,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dDraftWork.profileId),
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(
    await screen.findByRole('button', { name: /继续创作《水果抓大鹅》/u }),
  );
  expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '试玩' }));

  await waitFor(() => {
    expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
      'match3d-profile-draft-2d-1',
      ISOLATED_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(
    await screen.findByTestId('match3d-runtime-generated-model-count'),
  ).toHaveProperty('textContent', '0');
  await waitFor(() => {
    expect(
      screen.getByTestId('match3d-runtime-generated-item-image-count'),
    ).toHaveProperty('textContent', '1');
    expect(
      screen.getByTestId('match3d-runtime-generated-asset-count'),
    ).toHaveProperty('textContent', '1');
  });
});

test('match3d result back returns to draft hub when opened from shelf', async () => {
  const user = userEvent.setup();
  const match3dDraftWork: Match3DWorkSummary = {
    workId: 'match3d-work-back-1',
    profileId: 'match3d-profile-back-1',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-session-back-1',
    gameName: '自动试玩抓大鹅',
    themeText: '水果',
    summary: '',
    tags: ['水果', '休闲', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-12T12:10:00.000Z',
    publishedAt: null,
    publishReady: false,
    generatedItemAssets: [],
  };

  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [match3dDraftWork],
  });
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: buildMockMatch3DAgentSession({
      sessionId: 'match3d-session-back-1',
      draft: {
        profileId: 'match3d-profile-back-1',
        gameName: '自动试玩抓大鹅',
        themeText: '水果',
        summary: '',
        tags: ['水果', '休闲', '抓大鹅'],
        coverImageSrc: null,
        referenceImageSrc: null,
        clearCount: 12,
        difficulty: 4,
        generatedItemAssets: [],
      },
    }),
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(
    await screen.findByRole('button', { name: /继续创作《自动试玩抓大鹅》/u }),
  );

  expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回' }));

  const draftPanel = await findPlatformTabPanel('saves');
  await waitFor(() => {
    expect(draftPanel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(
    within(draftPanel).getByRole('tablist', { name: '作品筛选' }),
  ).toBeTruthy();
  expect(within(draftPanel).getByText('自动试玩抓大鹅')).toBeTruthy();
  expect(getPlatformTabPanel('create').getAttribute('aria-hidden')).toBe(
    'true',
  );
  expect(screen.queryByText('抓大鹅结果页')).toBeNull();
});

test('match3d draft generation auto starts trial and runtime back opens draft result', async () => {
  const user = userEvent.setup();
  const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
    {
      itemId: 'match3d-item-1',
      itemName: '草莓',
      imageSrc:
        '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
      imageObjectKey:
        'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
      modelSrc: null,
      modelObjectKey:
        'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
      modelFileName: 'strawberry.glb',
      taskUuid: 'task-strawberry',
      subscriptionKey: 'sub-strawberry',
      status: 'model_ready',
      error: null,
      backgroundAsset: match3DGeneratedUiAsset,
    },
  ];
  const generatedSession = buildMockMatch3DAgentSession({
    stage: 'draft_ready',
    draft: {
      profileId: 'match3d-profile-auto-1',
      gameName: '自动试玩抓大鹅',
      themeText: '水果',
      summary: '',
      tags: ['水果', '抓大鹅', '试玩'],
      coverImageSrc: null,
      referenceImageSrc: null,
      clearCount: 12,
      difficulty: 4,
      generatedItemAssets,
    },
  });
  const generatedProfile: Match3DWorkSummary = {
    workId: 'match3d-work-auto-1',
    profileId: 'match3d-profile-auto-1',
    ownerUserId: 'user-1',
    sourceSessionId: generatedSession.sessionId,
    gameName: '自动试玩抓大鹅',
    themeText: '水果',
    summary: '',
    tags: ['水果', '抓大鹅', '试玩'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-12T10:00:00.000Z',
    publishedAt: null,
    publishReady: false,
    generatedItemAssets,
  };

  vi.mocked(match3dCreationClient.executeAction).mockResolvedValueOnce({
    session: generatedSession,
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: generatedProfile,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(generatedProfile.profileId),
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );

  expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
  expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
    'match3d-profile-auto-1',
  );
  expect(
    await screen.findByTestId('match3d-runtime-generated-model-count'),
  ).toHaveProperty('textContent', '1');
  await waitFor(() => {
    expect(
      screen.getByTestId('match3d-runtime-top-level-background-count'),
    ).toHaveProperty('textContent', '1');
  });
  expect(
    screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
  ).toHaveBeenCalledWith(
    expect.any(Array),
    expect.objectContaining({
      imageSrc:
        '/generated-match3d-assets/session/profile/background/background.png',
      containerImageSrc:
        '/generated-match3d-assets/session/profile/ui-container/container.png',
    }),
    { expireSeconds: 300 },
  );

  await user.click(screen.getByRole('button', { name: '返回' }));

  expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
  expect(screen.getByText('自动试玩抓大鹅')).toBeTruthy();
});

test('match3d result trial loads generated background and container assets', async () => {
  const user = userEvent.setup();
  const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
    {
      itemId: 'match3d-trial-item-1',
      itemName: '草莓',
      imageSrc:
        '/generated-match3d-assets/session/profile/items/match3d-trial-item-1-item/image.png',
      imageObjectKey:
        'generated-match3d-assets/session/profile/items/match3d-trial-item-1-item/image.png',
      imageViews: [],
      modelSrc: null,
      modelObjectKey: null,
      modelFileName: null,
      taskUuid: null,
      subscriptionKey: null,
      status: 'image_ready',
      error: null,
      backgroundAsset: match3DGeneratedUiAsset,
    },
  ];
  const match3dDraftWork: Match3DWorkSummary = {
    workId: 'match3d-work-trial-ui',
    profileId: 'match3d-profile-trial-ui',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-session-trial-ui',
    gameName: '手动试玩抓大鹅',
    themeText: '水果',
    summary: '',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-14T11:00:00.000Z',
    publishedAt: null,
    publishReady: false,
    generatedBackgroundAsset: null,
    generatedItemAssets,
  };

  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [match3dDraftWork],
  });
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: buildMockMatch3DAgentSession({
      sessionId: 'match3d-session-trial-ui',
      stage: 'draft_ready',
      draft: {
        profileId: 'match3d-profile-trial-ui',
        gameName: '手动试玩抓大鹅',
        themeText: '水果',
        summary: '',
        tags: ['水果', '抓大鹅'],
        coverImageSrc: null,
        referenceImageSrc: null,
        clearCount: 12,
        difficulty: 4,
        generatedItemAssets,
      },
    }),
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: match3dDraftWork,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dDraftWork.profileId),
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(
    await screen.findByRole('button', { name: /继续创作《手动试玩抓大鹅》/u }),
  );
  expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '试玩' }));

  expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
  await waitFor(() => {
    expect(
      screen.getByTestId('match3d-runtime-top-level-background-count'),
    ).toHaveProperty('textContent', '1');
  });
  expect(
    screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
  ).toHaveBeenCalledWith(
    expect.any(Array),
    expect.objectContaining({
      imageSrc:
        '/generated-match3d-assets/session/profile/background/background.png',
      containerImageSrc:
        '/generated-match3d-assets/session/profile/ui-container/container.png',
    }),
    { expireSeconds: 300 },
  );
});

test('completed match3d draft notice first opens trial then reopens result', async () => {
  const user = userEvent.setup();
  const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
    {
      itemId: 'match3d-notice-item-1',
      itemName: '草莓',
      imageSrc:
        '/generated-match3d-assets/session/profile/items/match3d-notice-item-1-item/image.png',
      imageObjectKey:
        'generated-match3d-assets/session/profile/items/match3d-notice-item-1-item/image.png',
      imageViews: [],
      modelSrc: null,
      modelObjectKey: null,
      modelFileName: null,
      taskUuid: 'task-notice-strawberry',
      subscriptionKey: 'sub-notice-strawberry',
      status: 'image_ready',
      error: null,
      backgroundAsset: match3DGeneratedUiAsset,
    },
  ];
  const runningSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-notice-session-1',
    draft: null,
    stage: 'collecting_config',
  });
  const generatedSession = buildMockMatch3DAgentSession({
    sessionId: 'match3d-notice-session-1',
    stage: 'draft_ready',
    draft: {
      profileId: 'match3d-notice-profile-1',
      gameName: '红点自动试玩抓大鹅',
      themeText: '水果',
      summary: '',
      tags: ['水果', '抓大鹅'],
      coverImageSrc: null,
      referenceImageSrc: null,
      clearCount: 12,
      difficulty: 4,
      generatedItemAssets,
    },
  });
  const generatedProfile: Match3DWorkSummary = {
    workId: 'match3d-notice-work-1',
    profileId: 'match3d-notice-profile-1',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-notice-session-1',
    gameName: '红点自动试玩抓大鹅',
    themeText: '水果',
    summary: '',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-14T10:00:00.000Z',
    publishedAt: null,
    publishReady: false,
    generatedItemAssets,
  };
  vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
    session: runningSession,
  });
  let resolveCompile!: (value: {
    session: Match3DAgentSessionSnapshot;
  }) => void;
  vi.mocked(match3dCreationClient.executeAction).mockReturnValue(
    new Promise((resolve) => {
      resolveCompile = resolve;
    }),
  );
  vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
    session: generatedSession,
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: generatedProfile,
  });
  vi.mocked(listMatch3DWorks).mockResolvedValue({
    items: [generatedProfile],
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(generatedProfile.profileId),
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('抓大鹅'));
  await user.click(
    await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
  );
  expect(
    await screen.findByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);
  await expectDraftHubGeneratingBadgeCountAtLeast(1);

  await act(async () => {
    resolveCompile({ session: generatedSession });
  });

  const completionDialog = await screen.findByRole('dialog', {
    name: '生成完成',
  });
  expect(
    within(completionDialog).getByText(/抓大鹅草稿 match3d-notice-session-1/u),
  ).toBeTruthy();
  expect(within(completionDialog).getByText(/生成任务已完成/u)).toBeTruthy();
  expect(
    within(completionDialog).getByRole('button', { name: '复制内容' }),
  ).toBeTruthy();
  await user.click(within(completionDialog).getByLabelText('关闭'));

  expect(await screen.findByLabelText('新生成完成')).toBeTruthy();
  await user.click(
    await screen.findByRole('button', {
      name: /继续创作《红点自动试玩抓大鹅》/u,
    }),
  );

  expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
  expect(
    screen.queryByRole('progressbar', {
      name: '抓大鹅草稿生成进度',
    }),
  ).toBeNull();
  expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledTimes(1);
  await waitFor(() => {
    expect(
      screen.getByTestId('match3d-runtime-top-level-background-count'),
    ).toHaveProperty('textContent', '1');
  });
  expect(
    screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
  ).toHaveProperty('textContent', '1');
  await user.click(screen.getByRole('button', { name: '返回' }));
  expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: '返回' }));

  await openDraftHub(user);
  expect(screen.queryByLabelText('新生成完成')).toBeNull();
  await user.click(
    await screen.findByRole('button', {
      name: /继续创作《红点自动试玩抓大鹅》/u,
    }),
  );

  expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
  expect(screen.queryByText(/抓大鹅运行态/u)).toBeNull();
  expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledTimes(1);
});

test('completed baby object match draft viewed immediately does not keep unread marker', async () => {
  const user = userEvent.setup();
  const generatedDraft = buildMockBabyObjectMatchDraft();
  vi.mocked(createBabyObjectMatchDraft).mockImplementation(
    async (payload: CreateBabyObjectMatchDraftRequest) => ({
      draft: buildMockBabyObjectMatchDraft({
        itemNames: [payload.itemAName, payload.itemBName],
      }),
    }),
  );
  vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([generatedDraft]);

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('宝贝识物'));
  await user.type(await screen.findByLabelText('物品 A'), '苹果');
  await user.type(await screen.findByLabelText('物品 B'), '香蕉');
  await user.click(screen.getByRole('button', { name: '生成宝贝识物草稿' }));

  expect(await screen.findByText('宝贝识物结果页')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回' }));
  await waitFor(() => {
    expect(screen.queryByText('宝贝识物结果页')).toBeNull();
  });
  expect(await screen.findByLabelText('物品 A')).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: '返回' }));

  await openDraftHub(user);
  expect(
    await screen.findByRole('button', {
      name: /继续创作《宝贝识物红点草稿》/u,
    }),
  ).toBeTruthy();
  expect(screen.queryByLabelText('新生成完成')).toBeNull();

  await user.click(
    screen.getByRole('button', {
      name: /继续创作《宝贝识物红点草稿》/u,
    }),
  );

  expect(await screen.findByText('宝贝识物结果页')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回' }));
  await waitFor(() => {
    expect(screen.queryByText('宝贝识物结果页')).toBeNull();
  });
  const reopenedDraftPanel = await findPlatformTabPanel('saves');
  await waitFor(() => {
    expect(reopenedDraftPanel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(screen.queryByLabelText('新生成完成')).toBeNull();
});

test('completed baby object match draft shows unread marker after leaving generation page', async () => {
  const user = userEvent.setup();
  const generatedDraft = buildMockBabyObjectMatchDraft();
  let resolveCreateDraft!: (value: { draft: BabyObjectMatchDraft }) => void;
  vi.mocked(createBabyObjectMatchDraft).mockReturnValue(
    new Promise((resolve) => {
      resolveCreateDraft = resolve;
    }),
  );
  vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([generatedDraft]);

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('宝贝识物'));
  await user.type(await screen.findByLabelText('物品 A'), '苹果');
  await user.type(await screen.findByLabelText('物品 B'), '香蕉');
  await user.click(screen.getByRole('button', { name: '生成宝贝识物草稿' }));

  expect(
    await screen.findByRole('progressbar', {
      name: '宝贝识物草稿生成进度',
    }),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));
  await openDraftHub(user);

  await act(async () => {
    resolveCreateDraft({ draft: generatedDraft });
  });

  expect(await screen.findByLabelText('新生成完成')).toBeTruthy();
  expect(
    await screen.findByRole('button', { name: '草稿，有新草稿' }),
  ).toBeTruthy();

  await user.click(
    await screen.findByRole('button', {
      name: /继续创作《宝贝识物红点草稿》/u,
    }),
  );

  expect(await screen.findByText('宝贝识物结果页')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回' }));
  await waitFor(() => {
    expect(screen.queryByText('宝贝识物结果页')).toBeNull();
  });
  const draftPanel = await findPlatformTabPanel('saves');
  await waitFor(() => {
    expect(draftPanel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(screen.queryByLabelText('新生成完成')).toBeNull();
});

test('puzzle draft generation auto starts trial and runtime back opens draft result', async () => {
  const user = userEvent.setup();
  const generatedDraft = buildReadyPuzzleDraft({
    workTitle: '自动试玩拼图',
    workDescription: '生成完成后直接试玩。',
    coverImageSrc: '/puzzle/auto-candidate.png',
    levels: [
      {
        ...buildReadyPuzzleDraft().levels![0]!,
        coverImageSrc: '/puzzle/auto-candidate.png',
        uiBackgroundImageSrc:
          '/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
        uiBackgroundImageObjectKey:
          'generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
        backgroundMusic: {
          taskId: 'music-task-auto-1',
          provider: 'vector-engine-suno',
          assetObjectId: 'asset-music-auto-1',
          assetKind: 'puzzle_background_music',
          audioSrc:
            '/generated-puzzle-assets/puzzle-session-auto-1/audio/background.mp3',
          prompt: '',
          title: '水果乐园',
          updatedAt: '2026-05-14T10:00:00.000Z',
        },
      },
    ],
  });
  const generatedSession: PuzzleAgentSessionSnapshot = {
    sessionId: 'puzzle-session-auto-1',
    seedText: '屋檐下的猫与暖灯街角。',
    currentTurn: 1,
    progressPercent: 100,
    stage: 'ready_to_publish',
    anchorPack: buildPuzzleAnchorPack(),
    draft: generatedDraft,
    messages: [],
    lastAssistantReply: '拼图草稿已经生成。',
    publishedProfileId: null,
    suggestedActions: [],
    resultPreview: {
      draft: generatedDraft,
      publishReady: true,
      blockers: [],
      qualityFindings: [],
    },
    updatedAt: '2026-05-12T10:00:00.000Z',
  };

  vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
    operation: {
      operationId: 'compile-puzzle-auto-1',
      type: 'compile_puzzle_draft',
      status: 'completed',
      phaseLabel: '已完成',
      phaseDetail: '草稿已生成',
      progress: 1,
    },
    session: generatedSession,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  await waitFor(() => {
    expect(updatePuzzleWork).toHaveBeenCalledWith(
      'puzzle-profile-auto-1',
      expect.objectContaining({
        levelName: '雨夜猫街',
        coverImageSrc: '/puzzle/auto-candidate.png',
        levels: [
          expect.objectContaining({
            uiBackgroundImageSrc:
              '/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
            backgroundMusic: expect.objectContaining({
              audioSrc:
                '/generated-puzzle-assets/puzzle-session-auto-1/audio/background.mp3',
            }),
          }),
        ],
      }),
    );
  });
  await waitFor(() => {
    expect(startLocalPuzzleRun).toHaveBeenCalledTimes(1);
  });
  const runtimeWork = vi.mocked(startLocalPuzzleRun).mock.calls[0]?.[0];
  expect(runtimeWork?.levels?.[0]).toEqual(
    expect.objectContaining({
      uiBackgroundImageSrc:
        '/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
      uiBackgroundImageObjectKey:
        'generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
    }),
  );
  const runtimeSnapshot = vi.mocked(startLocalPuzzleRun).mock.results[0]?.value;
  expect(runtimeSnapshot?.currentLevel?.uiBackgroundImageSrc).toBe(
    '/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
  );
  expect(screen.queryByText('拼图结果页')).toBeNull();
  await waitFor(() => {
    expect(window.location.pathname).toBe('/runtime/puzzle');
    expect(window.location.search).toBe(
      '?runtimeProfileId=puzzle-profile-auto-1&runtimeSessionId=puzzle-session-auto-1&mode=draft',
    );
  });

  await act(async () => {
    window.history.back();
  });
  await waitFor(() => {
    expect(window.location.pathname).toBe('/creation/puzzle/result');
  });
  await user.click(await screen.findByRole('button', { name: '返回上一页' }));

  expect(await screen.findByText('拼图结果页')).toBeTruthy();
  expect(screen.getByDisplayValue('雨夜猫街')).toBeTruthy();
  const creationParams = new URLSearchParams(window.location.search);
  expect(creationParams.get('sessionId')).toBe('puzzle-session-auto-1');
  expect(creationParams.get('profileId')).toBe('puzzle-profile-auto-1');
});

test('embedded puzzle form recovers when compile request times out after backend completion', async () => {
  const user = userEvent.setup();
  const generatedDraft = buildReadyPuzzleDraft();
  const generatedSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-session-recovered',
    stage: 'ready_to_publish',
    progressPercent: 100,
    draft: generatedDraft,
    lastAssistantReply: '拼图草稿已经生成。',
    resultPreview: {
      draft: generatedDraft,
      publishReady: true,
      blockers: [],
      qualityFindings: [],
    },
    updatedAt: '2026-05-12T10:00:00.000Z',
  });

  vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
    session: buildMockPuzzleAgentSession({
      sessionId: 'puzzle-session-recovered',
    }),
  });
  vi.mocked(executePuzzleAgentAction).mockRejectedValueOnce(
    Object.assign(new Error('请求超时：90000ms'), {
      name: 'TimeoutError',
    }),
  );
  vi.mocked(getPuzzleAgentSession).mockResolvedValueOnce({
    session: generatedSession,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  await waitFor(() => {
    expect(getPuzzleAgentSession).toHaveBeenCalledWith(
      'puzzle-session-recovered',
    );
  });
  await waitFor(() => {
    expect(updatePuzzleWork).toHaveBeenCalledWith(
      'puzzle-profile-recovered',
      expect.objectContaining({
        levelName: '雨夜猫街',
        coverImageSrc: '/puzzle/recovered-candidate.png',
      }),
    );
  });
  expect(screen.queryByText('执行拼图操作失败。')).toBeNull();
  expect(screen.queryByText('请求超时：90000ms')).toBeNull();
  expect(screen.queryByText('拼图图片生成进度')).toBeNull();
  expect(startLocalPuzzleRun).toHaveBeenCalledTimes(1);
});

test('embedded puzzle form routes through requireAuth while logged out', async () => {
  const user = userEvent.setup();
  const requireAuth = vi.fn();

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        openLoginModal: () => {},
        requireAuth,
      })}
    />,
  );

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));

  expect(requireAuth).toHaveBeenCalledTimes(1);
  expect(screen.queryByText('拼图工作区：missing-session')).toBeNull();
  expect(createCreativeAgentSession).not.toHaveBeenCalled();
  expect(streamCreativeAgentMessage).not.toHaveBeenCalled();
  expect(createRpgCreationSession).not.toHaveBeenCalled();
  expect(createPuzzleAgentSession).not.toHaveBeenCalled();
  expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
});

test('opening RPG agent workspace does not refetch session snapshot in a render loop', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '补齐关键锚点',
      summary: '玩家是失职返乡的守灯人。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'clarifying',
      stageLabel: '补齐关键锚点',
      playableNpcCount: 0,
      landmarkCount: 0,
      roleVisualReadyCount: 0,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(getRpgCreationResultView).mockResolvedValue({
    ...buildResultViewForSession(mockSession),
    targetStage: 'agent-workspace',
    resultViewSource: null,
  });

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续创作/u);

  expect(
    await screen.findByText(
      'Agent工作区：custom-world-agent-session-1',
      {},
      { timeout: 5000 },
    ),
  ).toBeTruthy();

  await new Promise((resolve) => {
    window.setTimeout(resolve, 120);
  });

  expect(getRpgCreationSession).toHaveBeenCalledTimes(1);
  expect(createRpgCreationSession).not.toHaveBeenCalled();
});

test('create tab opens compiled agent draft in result refinement page', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '待完善草稿',
      summary: '玩家是失职返乡的守灯人。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'object_refining',
      stageLabel: '待完善草稿',
      playableNpcCount: 3,
      landmarkCount: 4,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: '沈砺 · 主图已生成',
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(getRpgCreationSession).mockResolvedValue(compiledAgentDraftSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(compiledAgentDraftSession),
  );

  render(<TestWrapper withAuth />);

  await openDraftHub(user);

  expect(await screen.findByRole('button', { name: /继续完善/u })).toBeTruthy();

  await user.click(await screen.findByRole('button', { name: /继续完善/u }));

  await waitFor(
    () => {
      expect(screen.queryByText('正在加载世界编辑器...')).toBeNull();
    },
    { timeout: 5000 },
  );

  expect(
    await screen.findByText('世界档案', {}, { timeout: 5000 }),
  ).toBeTruthy();
  expect(
    screen.queryByText('Agent工作区：custom-world-agent-session-1'),
  ).toBeNull();
  expect(screen.getByRole('button', { name: /返回创作/u })).toBeTruthy();
}, 10000);

test('create tab resumes agent workspace when draft has no compiled result yet', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '补齐关键锚点',
      summary: '玩家是失职返乡的守灯人。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'clarifying',
      stageLabel: '补齐关键锚点',
      playableNpcCount: 0,
      landmarkCount: 0,
      roleVisualReadyCount: 0,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);

  render(<TestWrapper withAuth />);

  await openDraftHub(user);

  expect(await screen.findByRole('button', { name: /继续创作/u })).toBeTruthy();

  await user.click(await screen.findByRole('button', { name: /继续创作/u }));

  expect(
    await screen.findByText('Agent工作区：custom-world-agent-session-1'),
  ).toBeTruthy();
  expect(screen.queryByText('世界档案')).toBeNull();
});

test('create tab resumes agent workspace when session has no draft profile even if summary counts look compiled', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '待完善草稿',
      summary: '作品卡摘要仍带着旧对象数量，但服务端还没有草稿 profile。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'clarifying',
      stageLabel: '补齐关键锚点',
      playableNpcCount: 2,
      landmarkCount: 1,
      roleVisualReadyCount: 0,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(getRpgCreationSession).mockResolvedValue({
    ...mockSession,
    stage: 'clarifying',
    draftProfile: null,
  });
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession({
      ...mockSession,
      stage: 'clarifying',
      draftProfile: null,
    }),
  );

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  expect(await screen.findByRole('button', { name: /继续完善/u })).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: /继续完善/u }));

  expect(
    await screen.findByText('Agent工作区：custom-world-agent-session-1'),
  ).toBeTruthy();
  expect(screen.queryByText('世界档案')).toBeNull();
});

test('opening a compiled draft with a missing agent session falls back to draft hub', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks)
    .mockResolvedValueOnce([
      {
        workId: 'draft:custom-world-agent-session-missing',
        sourceType: 'agent_session',
        status: 'draft',
        title: '潮雾列岛',
        subtitle: '世界底稿已生成',
        summary: '这是一份已经整理过首版结果页的草稿。',
        coverImageSrc: null,
        coverRenderMode: 'image',
        coverCharacterImageSrcs: [],
        updatedAt: '2026-04-20T11:00:00.000Z',
        publishedAt: null,
        stage: 'object_refining',
        stageLabel: '整理关键对象',
        playableNpcCount: 1,
        landmarkCount: 1,
        roleVisualReadyCount: 0,
        roleAnimationReadyCount: 0,
        roleAssetSummaryLabel: null,
        sessionId: 'custom-world-agent-session-missing',
        profileId: null,
        canResume: true,
        canEnterWorld: false,
      },
    ])
    .mockResolvedValueOnce([]);

  const missingSessionError = new ApiClientError({
    message: 'custom world agent session not found',
    status: 404,
    code: 'NOT_FOUND',
  });
  vi.mocked(getRpgCreationSession).mockRejectedValueOnce(missingSessionError);
  vi.mocked(getRpgCreationResultView).mockRejectedValueOnce(
    missingSessionError,
  );

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /继续完善/u }));

  const fallbackDraftPanel = getPlatformTabPanel('saves');
  await waitFor(() => {
    expect(fallbackDraftPanel.getAttribute('aria-hidden')).toBe('false');
    expect(
      screen.getAllByText(
        '这份共创草稿已失效，已为你返回草稿列表，请重新开始创作。',
      ).length,
    ).toBeGreaterThan(0);
  });

  expect(window.location.search).toBe('');
  expect(listRpgCreationWorks).toHaveBeenCalledTimes(2);
  expect(within(fallbackDraftPanel).getByText('还没有作品')).toBeTruthy();
  expect(
    screen.queryByText('Agent工作区：custom-world-agent-session-missing'),
  ).toBeNull();
});

test('clicking a public work while logged out opens public detail without starting RPG', async () => {
  const user = userEvent.setup();
  const requireAuth = vi.fn();

  vi.mocked(listRpgEntryWorldGallery).mockResolvedValue([
    {
      ownerUserId: 'author-1',
      profileId: 'world-public-1',
      publicWorkCode: 'work-public-1',
      authorPublicUserCode: 'author-1',
      visibility: 'published',
      publishedAt: '2026-04-16T12:00:00.000Z',
      updatedAt: '2026-04-16T12:00:00.000Z',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '最近公开发布的世界。',
      coverImageSrc: null,
      themeMode: 'tide',
      authorDisplayName: '潮汐作者',
      playableNpcCount: 3,
      landmarkCount: 4,
      likeCount: 0,
    },
  ]);

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        openLoginModal: () => {},
        requireAuth,
      })}
    />,
  );

  await openDiscoverHub(user);
  const workCards = await screen.findAllByRole('button', {
    name: /潮雾列岛/u,
  });
  await user.click(workCards[0]!);

  expect(await screen.findByText('详情')).toBeTruthy();
  expect(requireAuth).not.toHaveBeenCalled();
  await user.click(screen.getByRole('button', { name: '启动' }));
  expect(requireAuth).toHaveBeenCalledTimes(1);
  expect(recordRpgEntryWorldGalleryPlay).not.toHaveBeenCalled();
});

test('logged out public detail gates puzzle start and remix before real actions', async () => {
  const user = userEvent.setup();
  const requireAuth = vi.fn();
  const publishedPuzzleWork = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '星桥机关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    remixCount: 0,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [publishedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: publishedPuzzleWork,
  });

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        canAccessProtectedData: false,
        openLoginModal: () => {},
        requireAuth,
      })}
    />,
  );

  await openDiscoverHub(user);

  const workCards = screen.getAllByRole('button', { name: /星桥机关/u });
  await user.click(workCards[0]!);
  expect(await screen.findByText('详情')).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '启动' }));
  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: 'puzzle-profile-public-1',
        levelId: null,
      },
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
      }),
    );
  });
  expect(requireAuth).toHaveBeenCalledTimes(1);
  requireAuth.mockClear();

  await user.click(screen.getByRole('button', { name: '作品改造' }));
  expect(requireAuth).toHaveBeenCalledTimes(1);
  expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
});

test('logged out public jump-hop detail starts runtime without requireAuth', async () => {
  const user = userEvent.setup();
  const requireAuth = vi.fn();
  const publishedJumpHopWork: JumpHopWorkProfileResponse = {
    summary: {
      runtimeKind: 'jump-hop',
      workId: 'jump-hop-work-public-1',
      profileId: 'jump-hop-profile-public-12345678',
      ownerUserId: 'user-2',
      sourceSessionId: 'jump-hop-session-public-1',
      themeText: '云上方块',
      workTitle: '云上方块跳一跳',
      workDescription: '在云层地块之间连续弹跳。',
      themeTags: ['云层', '跳跃'],
      difficulty: 'standard',
      stylePreset: 'paper-toy',
      coverImageSrc: null,
      publicationStatus: 'published',
      playCount: 3,
      updatedAt: '2026-05-29T10:00:00.000Z',
      publishedAt: '2026-05-29T10:00:00.000Z',
      publishReady: true,
      generationStatus: 'ready',
    },
    draft: {
      templateId: 'jump-hop',
      templateName: '跳一跳',
      profileId: 'jump-hop-profile-public-12345678',
      themeText: '云上方块',
      workTitle: '云上方块跳一跳',
      workDescription: '在云层地块之间连续弹跳。',
      themeTags: ['云层', '跳跃'],
      difficulty: 'standard',
      stylePreset: 'paper-toy',
      defaultCharacter: {
        characterId: 'builtin-default',
        displayName: '默认角色',
        modelKind: 'builtin-three',
        bodyColor: '#df7f40',
        accentColor: '#2563eb',
      },
      characterPrompt: '',
      tilePrompt: '云上方块',
      endMoodPrompt: null,
      characterAsset: null,
      tileAtlasAsset: null,
      tileAssets: [],
      path: null,
      coverComposite: null,
      generationStatus: 'ready',
    },
    path: {
      seed: 'jump-hop-public-seed',
      difficulty: 'standard',
      platforms: [
        {
          platformId: 'platform-0',
          tileType: 'start',
          x: 0,
          y: 0,
          width: 1,
          height: 1,
          landingRadius: 0.7,
          perfectRadius: 0.25,
          scoreValue: 1,
        },
        {
          platformId: 'platform-1',
          tileType: 'normal',
          x: 1,
          y: 1,
          width: 1,
          height: 1,
          landingRadius: 0.7,
          perfectRadius: 0.25,
          scoreValue: 1,
        },
        {
          platformId: 'platform-2',
          tileType: 'normal',
          x: -1,
          y: 2,
          width: 1,
          height: 1,
          landingRadius: 0.7,
          perfectRadius: 0.25,
          scoreValue: 1,
        },
      ],
      finishIndex: 2,
      cameraPreset: 'portrait-top-down',
      scoring: {
        chargeToDistanceRatio: 1,
        maxChargeMs: 1800,
        hitBonus: 0,
        perfectBonus: 0,
      },
    },
    defaultCharacter: {
      characterId: 'builtin-default',
      displayName: '默认角色',
      modelKind: 'builtin-three',
      bodyColor: '#df7f40',
      accentColor: '#2563eb',
    },
    characterAsset: {
      assetId: 'builtin-character',
      imageSrc: '',
      imageObjectKey: '',
      assetObjectId: '',
      generationProvider: 'builtin',
      prompt: '',
      width: 1,
      height: 1,
    },
    tileAtlasAsset: {
      assetId: 'tile-atlas-1',
      imageSrc: '/generated-jump-hop-assets/public/atlas.png',
      imageObjectKey: 'generated-jump-hop-assets/public/atlas.png',
      assetObjectId: 'asset-tile-atlas-1',
      generationProvider: 'gpt-image-2',
      prompt: '云上方块',
      width: 1024,
      height: 1024,
    },
    tileAssets: [],
  };
  const publishedJumpHopRun: JumpHopRuntimeRunSnapshotResponse = {
    runId: 'jump-hop-run-public-1',
    profileId: publishedJumpHopWork.summary.profileId,
    ownerUserId: '',
    status: 'playing',
    currentPlatformIndex: 0,
    successfulJumpCount: 0,
    durationMs: 0,
    score: 0,
    combo: 0,
    path: publishedJumpHopWork.path,
    lastJump: null,
    startedAtMs: 1_779_999_000_000,
    finishedAtMs: null,
  };

  window.history.replaceState(null, '', '/works/detail?work=JH-12345678');
  vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
    items: [
      {
        publicWorkCode: 'JH-12345678',
        workId: publishedJumpHopWork.summary.workId,
        profileId: publishedJumpHopWork.summary.profileId,
        ownerUserId: publishedJumpHopWork.summary.ownerUserId,
        authorDisplayName: '跳跃作者',
        themeText: publishedJumpHopWork.summary.themeText,
        workTitle: publishedJumpHopWork.summary.workTitle,
        workDescription: publishedJumpHopWork.summary.workDescription,
        coverImageSrc: null,
        themeTags: publishedJumpHopWork.summary.themeTags,
        difficulty: publishedJumpHopWork.summary.difficulty,
        stylePreset: publishedJumpHopWork.summary.stylePreset,
        publicationStatus: publishedJumpHopWork.summary.publicationStatus,
        playCount: publishedJumpHopWork.summary.playCount,
        updatedAt: publishedJumpHopWork.summary.updatedAt,
        publishedAt: publishedJumpHopWork.summary.publishedAt,
        generationStatus: publishedJumpHopWork.summary.generationStatus,
      },
    ],
    hasMore: false,
    nextCursor: null,
  });
  vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValue({
    item: publishedJumpHopWork,
  });
  vi.mocked(jumpHopClient.startRun).mockResolvedValue({
    run: publishedJumpHopRun,
  });
  vi.mocked(jumpHopClient.getLeaderboard).mockResolvedValue({
    profileId: publishedJumpHopWork.summary.profileId,
    items: [],
    viewerBest: null,
  });

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        canAccessProtectedData: false,
        openLoginModal: () => {},
        requireAuth,
      })}
    />,
  );

  expect(await screen.findByText('详情')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '启动' }));

  await waitFor(() => {
    expect(jumpHopClient.startRun).toHaveBeenCalledWith(
      publishedJumpHopWork.summary.profileId,
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
        runtimeMode: 'published',
      }),
    );
  });
  expect(requireAuth).not.toHaveBeenCalled();
});

test('direct jump hop runtime route without work code returns platform home', async () => {
  window.history.replaceState(null, '', '/runtime/jump-hop');

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(window.location.pathname).toBe('/');
  });
  expect(window.location.search).toBe('');
  expect(jumpHopClient.getGalleryDetail).not.toHaveBeenCalled();
  expect(jumpHopClient.startRun).not.toHaveBeenCalled();
  expect(await screen.findByRole('button', { name: '创作' })).toBeTruthy();
});

test('direct jump hop runtime route with public work code starts published run', async () => {
  const publishedJumpHopWork = buildMockJumpHopWork({
    summary: {
      runtimeKind: 'jump-hop',
      workId: 'jump-hop-work-direct-1',
      profileId: 'jump-hop-profile-direct-12345678',
      ownerUserId: 'user-2',
      sourceSessionId: 'jump-hop-session-direct-1',
      themeText: '星星果园',
      workTitle: '星星果园跳一跳',
      workDescription: '沿着水果一路弹跳。',
      themeTags: ['果园', '星星'],
      difficulty: 'standard',
      stylePreset: 'paper-toy',
      coverImageSrc: null,
      publicationStatus: 'published',
      playCount: 8,
      updatedAt: '2026-06-07T10:00:00.000Z',
      publishedAt: '2026-06-07T10:00:00.000Z',
      publishReady: true,
      generationStatus: 'ready',
    },
  });
  const publishedJumpHopRun = buildMockJumpHopRuntimeRun(publishedJumpHopWork, {
    runId: 'jump-hop-run-direct-1',
    ownerUserId: '',
  });

  window.history.replaceState(null, '', '/runtime/jump-hop?work=JH-12345678');
  vi.mocked(jumpHopClient.getGalleryDetail).mockResolvedValue({
    item: publishedJumpHopWork,
  });
  vi.mocked(jumpHopClient.startRun).mockResolvedValue({
    run: publishedJumpHopRun,
  });
  vi.mocked(jumpHopClient.getLeaderboard).mockResolvedValue({
    profileId: publishedJumpHopWork.summary.profileId,
    items: [],
    viewerBest: null,
  });

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        canAccessProtectedData: false,
        openLoginModal: () => {},
        requireAuth: vi.fn(),
      })}
    />,
  );

  await waitFor(() => {
    expect(jumpHopClient.getGalleryDetail).toHaveBeenCalledWith('JH-12345678');
  });
  await waitFor(() => {
    expect(jumpHopClient.startRun).toHaveBeenCalledWith(
      publishedJumpHopWork.summary.profileId,
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
        runtimeMode: 'published',
      }),
    );
  });
  expect(jumpHopClient.getWorkDetail).not.toHaveBeenCalled();
  expect(window.location.pathname).toBe('/runtime/jump-hop');
  expect(window.location.search).toContain('work=JH-12345678');
});

test('owned public puzzle detail edits original draft instead of remixing', async () => {
  const user = userEvent.setup();
  const ownedPuzzleWork = {
    workId: 'puzzle-work-owned-1',
    profileId: 'puzzle-profile-owned-1',
    ownerUserId: mockAuthUser.id,
    sourceSessionId: 'puzzle-session-1',
    authorDisplayName: mockAuthUser.displayName,
    levelName: '星桥机关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    remixCount: 0,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [ownedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: ownedPuzzleWork,
  });

  render(<TestWrapper withAuth />);

  await openDiscoverHub(user);
  await waitFor(() => {
    expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
  });

  const workCards = screen.getAllByRole('button', { name: /星桥机关/u });
  await user.click(workCards[0]!);
  expect(await screen.findByText('详情')).toBeTruthy();
  expect(screen.getByRole('button', { name: '作品编辑' })).toBeTruthy();
  expect(screen.queryByRole('button', { name: '作品改造' })).toBeNull();

  await user.click(screen.getByRole('button', { name: '作品编辑' }));

  expect(getPuzzleAgentSession).toHaveBeenCalledWith('puzzle-session-1');
  expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
  expect(await screen.findByText('拼图结果页')).toBeTruthy();
  const generatingPuzzleDraft: PuzzleResultDraft = {
    workTitle: '暖灯猫街作品',
    workDescription: '一套雨夜猫街主题拼图。',
    levelName: '雨夜猫街',
    summary: '屋檐下的猫与暖灯街角。',
    themeTags: ['猫咪', '雨夜', '暖灯'],
    forbiddenDirectives: [],
    creatorIntent: null,
    anchorPack: {
      themePromise: {
        key: 'theme_promise',
        label: '主题承诺',
        value: '雨夜猫街',
        status: 'confirmed',
      },
      visualSubject: {
        key: 'visual_subject',
        label: '视觉主体',
        value: '屋檐下的猫',
        status: 'confirmed',
      },
      visualMood: {
        key: 'visual_mood',
        label: '视觉气质',
        value: '温暖',
        status: 'confirmed',
      },
      compositionHooks: {
        key: 'composition_hooks',
        label: '构图钩子',
        value: '雨滴与灯牌',
        status: 'confirmed',
      },
      tagsAndForbidden: {
        key: 'tags_and_forbidden',
        label: '标签与禁区',
        value: '猫咪、雨夜',
        status: 'confirmed',
      },
    },
    candidates: [
      {
        candidateId: 'candidate-1',
        imageSrc: '/puzzle/candidate-1.png',
        assetId: 'asset-1',
        prompt: '雨夜猫咪',
        actualPrompt: null,
        sourceType: 'generated',
        selected: true,
      },
    ],
    selectedCandidateId: 'candidate-1',
    coverImageSrc: '/puzzle/candidate-1.png',
    coverAssetId: 'asset-1',
    generationStatus: 'generating',
    levels: [
      {
        levelId: 'puzzle-level-1',
        levelName: '雨夜猫街',
        pictureDescription: '屋檐下的猫与暖灯街角。',
        pictureReference: null,
        candidates: [
          {
            candidateId: 'candidate-1',
            imageSrc: '/puzzle/candidate-1.png',
            assetId: 'asset-1',
            prompt: '雨夜猫咪',
            actualPrompt: null,
            sourceType: 'generated',
            selected: true,
          },
        ],
        selectedCandidateId: 'candidate-1',
        coverImageSrc: '/puzzle/candidate-1.png',
        coverAssetId: 'asset-1',
        generationStatus: 'generating',
      },
    ],
    metadata: null,
  };

  vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
    operation: {
      operationId: 'puzzle-image-generation-1',
      type: 'generate_puzzle_images',
      status: 'running',
      phaseLabel: '生成中',
      phaseDetail: '正在生成拼图画面',
      progress: 0.3,
    },
    session: {
      sessionId: 'puzzle-session-1',
      currentTurn: 3,
      progressPercent: 88,
      stage: 'ready_to_publish',
      anchorPack: {
        themePromise: {
          key: 'theme_promise',
          label: '主题承诺',
          value: '雨夜猫街',
          status: 'confirmed',
        },
        visualSubject: {
          key: 'visual_subject',
          label: '视觉主体',
          value: '屋檐下的猫',
          status: 'confirmed',
        },
        visualMood: {
          key: 'visual_mood',
          label: '视觉气质',
          value: '温暖',
          status: 'confirmed',
        },
        compositionHooks: {
          key: 'composition_hooks',
          label: '构图钩子',
          value: '雨滴与灯牌',
          status: 'confirmed',
        },
        tagsAndForbidden: {
          key: 'tags_and_forbidden',
          label: '标签与禁区',
          value: '猫咪、雨夜',
          status: 'confirmed',
        },
      },
      draft: generatingPuzzleDraft,
      messages: [],
      lastAssistantReply: '大鱼结果页草稿已经生成，可以补正式资产。',
      publishedProfileId: null,
      suggestedActions: [],
      resultPreview: {
        draft: generatingPuzzleDraft,
        publishReady: false,
        blockers: [],
        qualityFindings: [],
      },
      updatedAt: '2026-04-26T10:10:00.000Z',
    },
  });

  await user.click(screen.getByRole('button', { name: '重新生成画面' }));

  expect(executePuzzleAgentAction).toHaveBeenCalledWith(
    'puzzle-session-1',
    expect.objectContaining({
      action: 'generate_puzzle_images',
    }),
  );
  expect(screen.getByRole('button', { name: '新增关卡' })).toHaveProperty(
    'disabled',
    false,
  );
});

test('logged out public detail gates big fish start before local runtime', async () => {
  const user = userEvent.setup();
  const requireAuth = vi.fn();
  const bigFishWork: BigFishWorkSummary = {
    workId: 'big-fish-work-public-1',
    sourceSessionId: 'big-fish-session-public-1',
    ownerUserId: 'user-2',
    authorDisplayName: '大鱼作者',
    title: '机械深海 大鱼吃小鱼',
    subtitle: '机械微生物吞并进化',
    summary: '从微光孢子一路吞并成长到深海巨鲲。',
    coverImageSrc: null,
    status: 'published',
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    levelCount: 8,
    levelMainImageReadyCount: 8,
    levelMotionReadyCount: 16,
    backgroundReady: true,
  };

  vi.mocked(listBigFishGallery).mockResolvedValue({
    items: [bigFishWork],
  });

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        canAccessProtectedData: false,
        openLoginModal: () => {},
        requireAuth,
      })}
    />,
  );

  await openDiscoverHub(user);
  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'BF-NPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  expect(await screen.findByText('详情')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '启动' }));

  expect(requireAuth).toHaveBeenCalledTimes(1);
  expect(startBigFishRun).not.toHaveBeenCalled();
  expect(recordBigFishPlay).not.toHaveBeenCalled();
});

test('public code search blocks edutainment work when entry switch is disabled', async () => {
  vi.stubEnv('VITE_ENABLE_EDUTAINMENT_ENTRY', 'false');
  const user = userEvent.setup();
  const edutainmentPuzzleWork: PuzzleWorkSummary = {
    workId: 'puzzle-work-edutainment-1',
    profileId: 'puzzle-profile-edutainment-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-edutainment-1',
    authorDisplayName: '动作 Demo 作者',
    levelName: '儿童动作热身 Demo',
    summary: '寓教于乐专属动作 Demo。',
    themeTags: ['运动', '安全', '拼图', '寓教于乐'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-05-09T10:00:00.000Z',
    publishedAt: '2026-05-09T10:00:00.000Z',
    playCount: 3,
    remixCount: 0,
    likeCount: 0,
    publishReady: true,
  };

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [edutainmentPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: edutainmentPuzzleWork,
  });

  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'PZ-TMENT1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  const dialog = await screen.findByRole('dialog', { name: '未找到结果' });
  expect(within(dialog).getByText('公开编号搜索')).toBeTruthy();
  expect(within(dialog).getByText('未找到拼图作品。')).toBeTruthy();
  await user.click(
    within(dialog).getByRole('button', { name: '关闭搜索结果' }),
  );

  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '未找到结果' })).toBeNull();
  });
  expect(screen.queryByText('儿童动作热身 Demo')).toBeNull();
  expect(getPuzzleGalleryDetail).not.toHaveBeenCalled();
});

test('public code search shows public user summary in shared search result modal and clears it on close', async () => {
  const user = userEvent.setup();
  vi.mocked(authServiceMocks.getPublicAuthUserByCode).mockResolvedValue({
    id: 'public-user-yjsh',
    publicUserCode: 'SY-00000001',
    username: 'author_user',
    displayName: '公开作者',
    avatarUrl: null,
  });

  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, '月井守望');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  const dialog = await screen.findByRole('dialog', { name: '命中用户' });
  expect(within(dialog).getByText('公开编号搜索')).toBeTruthy();
  expect(within(dialog).getByText('公开作者')).toBeTruthy();
  expect(within(dialog).getByText('陶泥号')).toBeTruthy();
  expect(within(dialog).getByText('SY-00000001')).toBeTruthy();

  await user.click(
    within(dialog).getByRole('button', { name: '关闭搜索结果' }),
  );

  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '命中用户' })).toBeNull();
  });
});

test('creation hub clears all private work shelves immediately after logout state', async () => {
  const user = userEvent.setup();
  const loggedInAuth = createAuthValue();
  const loggedOutAuth = createAuthValue({
    user: null,
    canAccessProtectedData: false,
    openLoginModal: () => {},
    requireAuth: () => {},
  });

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:rpg-logout-cache-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: 'RPG 退出缓存作品',
      subtitle: '登出后不应继续可见',
      summary: '这条 RPG 私有作品只能在登录态展示。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-25T10:00:00.000Z',
      publishedAt: null,
      stage: 'clarifying',
      stageLabel: '补齐关键锚点',
      playableNpcCount: 0,
      landmarkCount: 0,
      roleVisualReadyCount: 0,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'rpg-logout-cache-session',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(listBigFishWorks).mockResolvedValue({
    items: [
      {
        workId: 'big-fish-logout-cache-1',
        sourceSessionId: 'big-fish-logout-cache-session',
        ownerUserId: 'user-1',
        authorDisplayName: '测试玩家',
        title: '大鱼退出缓存作品',
        subtitle: '登出后不应继续可见',
        summary: '这条大鱼私有作品只能在登录态展示。',
        coverImageSrc: null,
        status: 'draft',
        updatedAt: '2026-04-25T10:05:00.000Z',
        publishReady: false,
        levelCount: 8,
        levelMainImageReadyCount: 0,
        levelMotionReadyCount: 0,
        backgroundReady: false,
      },
    ],
  });
  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [
      {
        workId: 'puzzle-logout-cache-1',
        profileId: 'puzzle-logout-cache-profile',
        ownerUserId: 'user-1',
        sourceSessionId: 'puzzle-logout-cache-session',
        authorDisplayName: '测试玩家',
        levelName: '拼图退出缓存作品',
        summary: '这条拼图私有作品只能在登录态展示。',
        themeTags: ['退出态'],
        coverImageSrc: null,
        publicationStatus: 'draft',
        updatedAt: '2026-04-25T10:10:00.000Z',
        publishedAt: null,
        playCount: 0,
        likeCount: 0,
        publishReady: false,
      },
    ],
  });

  const { rerender } = render(<TestWrapper authValue={loggedInAuth} />);

  await openDraftHub(user);
  const draftPanel = getPlatformTabPanel('saves');

  await waitFor(() => {
    expect(listRpgCreationWorks).toHaveBeenCalled();
  });
  expect(await within(draftPanel).findByText('拼图退出缓存作品')).toBeTruthy();
  expect(within(draftPanel).queryByText('RPG 退出缓存作品')).toBeNull();
  expect(within(draftPanel).queryByText('大鱼退出缓存作品')).toBeNull();

  rerender(<TestWrapper authValue={loggedOutAuth} />);

  await waitFor(() => {
    expect(screen.queryByText('RPG 退出缓存作品')).toBeNull();
    expect(screen.queryByText('拼图退出缓存作品')).toBeNull();
  });
});

test('creation draft hub skips visual novel shelves when entry is not open', async () => {
  const user = userEvent.setup();
  vi.mocked(fetchCreationEntryConfig).mockResolvedValue({
    ...testCreationEntryConfig,
    creationTypes: testCreationEntryConfig.creationTypes.map((entry) =>
      entry.id === 'visual-novel' ? { ...entry, open: false } : entry,
    ),
  });
  vi.mocked(listVisualNovelGallery).mockRejectedValue(
    new Error('该玩法入口暂不可用'),
  );
  vi.mocked(listVisualNovelWorks).mockRejectedValue(
    new Error('该玩法入口暂不可用'),
  );

  render(<TestWrapper withAuth />);

  await openDraftHub(user);

  expect(listVisualNovelGallery).not.toHaveBeenCalled();
  expect(listVisualNovelWorks).not.toHaveBeenCalled();
  expect(screen.queryByText('该玩法入口暂不可用')).toBeNull();
});

test('platform home suppresses creation entry disabled bootstrap errors', async () => {
  vi.mocked(listRpgEntryWorldLibrary).mockRejectedValue(
    new Error(
      '该玩法入口暂不可用 creation_entry_disabled（requestId: req-closed）',
    ),
  );
  vi.mocked(listRpgCreationWorks).mockRejectedValue(
    new Error('creation_entry_disabled'),
  );

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(listRpgEntryWorldLibrary).toHaveBeenCalled();
  });

  expect(screen.queryByText(/平台首页/u)).toBeNull();
  expect(screen.queryByText(/creation_entry_disabled/u)).toBeNull();
  expect(screen.queryByText(/该玩法入口暂不可用/u)).toBeNull();
});

test('published puzzle works appear on home and mobile game category channel', async () => {
  const user = userEvent.setup();
  const publishedPuzzleWork = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-public-1',
    authorDisplayName: '拼图作者',
    levelName: '星桥机关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [publishedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: publishedPuzzleWork,
  });

  render(<TestWrapper />);

  await waitFor(() => {
    expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
  });

  await clickFirstButtonByName(user, '发现');
  await user.click(screen.getByRole('tab', { name: '分类' }));

  const discoverPanel = getPlatformTabPanel('category');
  expect(within(discoverPanel).getAllByText('星桥机关').length).toBeGreaterThan(
    0,
  );
  expect(
    within(discoverPanel).getAllByRole('button', { name: /机关/u }).length,
  ).toBeGreaterThan(0);
  expect(screen.queryByRole('button', { name: 'PC游戏' })).toBeNull();
  expect(screen.queryByRole('button', { name: '即点即玩' })).toBeNull();
});

test('home recommendation starts embedded puzzle without global auth reset on local failure', async () => {
  const publishedPuzzleWork = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-public-1',
    authorDisplayName: '拼图作者',
    levelName: '星桥机关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [publishedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: publishedPuzzleWork,
  });

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: 'puzzle-profile-public-1',
        levelId: null,
      },
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
});

test('home recommendation share opens publish share modal', async () => {
  const user = userEvent.setup();
  const publishedPuzzleWork = {
    workId: 'puzzle-work-share-1',
    profileId: 'SHARE001',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-share-1',
    authorDisplayName: '拼图作者',
    levelName: '星桥分享关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [publishedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: publishedPuzzleWork,
  });

  render(<TestWrapper withAuth />);

  const meta = await screen.findByLabelText('星桥分享关 作品信息');
  await user.click(within(meta).getByRole('button', { name: '分享' }));

  expect(
    await screen.findByRole('dialog', { name: '分享给朋友' }),
  ).toBeTruthy();
  expect(screen.getByText(/作品号：PZ-SHARE001/u)).toBeTruthy();
  expect(
    screen.getByText(/\/gallery\/puzzle\/detail\?work=PZ-SHARE001/u),
  ).toBeTruthy();
});

test('home recommendation wooden fish like does not call RPG gallery like', async () => {
  const user = userEvent.setup();
  const publishedWoodenFishWork: WoodenFishGalleryCardResponse = {
    publicWorkCode: 'WF-3A9EC89B',
    workId: 'wooden-fish-work-like-1',
    profileId: 'wooden-fish-profile-like-1',
    ownerUserId: 'wooden-fish-user-1',
    authorDisplayName: '木鱼作者',
    workTitle: '莲台木鱼',
    workDescription: '推荐页里的敲木鱼作品。',
    coverImageSrc: null,
    themeTags: ['敲木鱼'],
    publicationStatus: 'published',
    playCount: 0,
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    generationStatus: 'ready',
  };

  vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
    items: [publishedWoodenFishWork],
    hasMore: false,
    nextCursor: null,
  });
  vi.mocked(woodenFishClient.startRun).mockResolvedValue({
    run: {
      runId: 'wooden-fish-run-like-1',
      profileId: publishedWoodenFishWork.profileId,
      ownerUserId: publishedWoodenFishWork.ownerUserId,
      status: 'playing',
      totalTapCount: 0,
      wordCounters: [],
      startedAtMs: 1,
      updatedAtMs: 1,
      finishedAtMs: null,
    },
  });
  vi.mocked(likeRpgEntryWorldGallery).mockResolvedValue(
    buildMockRpgGalleryDetail({
      ownerUserId: 'custom-world-user-1',
      profileId: 'custom-world-profile-1',
      publicWorkCode: 'CW-00000001',
      authorPublicUserCode: 'SY-00000001',
      visibility: 'published',
      publishedAt: '2026-04-25T09:00:00.000Z',
      updatedAt: '2026-04-25T09:00:00.000Z',
      authorDisplayName: 'RPG 作者',
      worldName: '不应被点赞的 RPG',
      subtitle: '错误分流',
      summaryText: 'WF 点赞不应进入这里。',
      coverImageSrc: null,
      themeMode: 'mythic',
      playableNpcCount: 0,
      landmarkCount: 0,
      likeCount: 1,
    }),
  );

  render(<TestWrapper withAuth />);

  const meta = await screen.findByLabelText('莲台木鱼 作品信息');
  await user.click(within(meta).getByRole('button', { name: '点赞 0' }));

  expect(likeRpgEntryWorldGallery).not.toHaveBeenCalled();
  expect(
    await screen.findByText('作品类型 wooden-fish 暂不支持点赞。'),
  ).toBeTruthy();
});

test('home recommendation keeps logged-in puzzle start on default auth instead of guest token', async () => {
  const publishedPuzzleWork = {
    workId: 'puzzle-work-public-2',
    profileId: 'puzzle-profile-public-2',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-public-2',
    authorDisplayName: '拼图作者',
    levelName: '星桥机关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [publishedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: publishedPuzzleWork,
  });

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: 'puzzle-profile-public-2',
        levelId: null,
      },
      expect.objectContaining({
        authImpact: 'local',
        skipRefresh: true,
        notifyAuthStateChange: false,
        clearAuthOnUnauthorized: false,
      }),
    );
  });
  expect(vi.mocked(startPuzzleRun).mock.calls[0]?.[1]).not.toHaveProperty(
    'runtimeGuestToken',
  );
});

test('logged out home recommendation next starts the next puzzle work', async () => {
  const user = userEvent.setup();
  const firstWork = {
    workId: 'puzzle-work-public-next-1',
    profileId: 'puzzle-profile-public-next-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-public-next-1',
    authorDisplayName: '拼图作者',
    levelName: '家常菜',
    summary: '酱猪蹄不是酱肘子。',
    themeTags: ['家常菜', '拼图'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T10:00:00.000Z',
    publishedAt: '2026-04-25T10:00:00.000Z',
    playCount: 47,
    likeCount: 1,
    publishReady: true,
  } satisfies PuzzleWorkSummary;
  const secondWork = {
    ...firstWork,
    workId: 'puzzle-work-public-next-2',
    profileId: 'puzzle-profile-public-next-2',
    ownerUserId: 'user-3',
    sourceSessionId: 'puzzle-session-public-next-2',
    authorDisplayName: '贝壳作者',
    levelName: '贝壳',
    summary: '第二个公开拼图。',
    themeTags: ['贝壳', '拼图'],
    playCount: 1,
    likeCount: 0,
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [firstWork, secondWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
    item: profileId === secondWork.profileId ? secondWork : firstWork,
  }));

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        canAccessProtectedData: false,
        openLoginModal: () => {},
        requireAuth: (action) => action(),
      })}
    />,
  );

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: firstWork.profileId,
        levelId: null,
      },
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
      }),
    );
  });
  expect(startPuzzleRun).toHaveBeenCalledTimes(1);

  await user.click(await screen.findByRole('button', { name: '下一个' }));

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: secondWork.profileId,
        levelId: null,
      },
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
      }),
    );
  });
});

test('home recommendation next follows the same scored queue shown in preview', async () => {
  const user = userEvent.setup();
  const quietWork = {
    workId: 'puzzle-work-public-quiet',
    profileId: 'puzzle-profile-public-quiet',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-public-quiet',
    authorDisplayName: '拼图作者',
    levelName: '安静拼图',
    summary: '列表里排在前面但热度较低。',
    themeTags: ['安静', '拼图'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T10:00:00.000Z',
    publishedAt: '2026-04-25T10:00:00.000Z',
    playCount: 40,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;
  const hotWork = {
    ...quietWork,
    workId: 'puzzle-work-public-hot',
    profileId: 'puzzle-profile-public-hot',
    sourceSessionId: 'puzzle-session-public-hot',
    levelName: '热门拼图',
    summary: '推荐评分更高，应该先展示。',
    playCount: 120,
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
  } satisfies PuzzleWorkSummary;
  const middleWork = {
    ...quietWork,
    workId: 'puzzle-work-public-middle',
    profileId: 'puzzle-profile-public-middle',
    sourceSessionId: 'puzzle-session-public-middle',
    levelName: '中间拼图',
    summary: '推荐评分排在后面。',
    playCount: 0,
    updatedAt: '2026-04-25T08:00:00.000Z',
    publishedAt: '2026-04-25T08:00:00.000Z',
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [quietWork, hotWork, middleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
    item:
      profileId === hotWork.profileId
        ? hotWork
        : profileId === middleWork.profileId
          ? middleWork
          : quietWork,
  }));
  vi.mocked(startPuzzleRun).mockImplementation(async (payload) => ({
    run: buildMockPuzzleRun(
      payload.profileId,
      payload.profileId === hotWork.profileId
        ? hotWork.levelName
        : payload.profileId === middleWork.profileId
          ? middleWork.levelName
          : quietWork.levelName,
    ),
  }));

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: hotWork.profileId,
        levelId: null,
      },
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(
    await screen.findByLabelText('热门拼图 作品信息', undefined, {
      timeout: 3000,
    }),
  ).toBeTruthy();
  const nextPreview = document.querySelector(
    '.platform-recommend-swipe-page--next',
  );
  expect(nextPreview).toBeTruthy();
  expect(
    within(nextPreview as HTMLElement).getByLabelText('安静拼图 作品信息'),
  ).toBeTruthy();

  await user.click(await screen.findByRole('button', { name: '下一个' }));

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: quietWork.profileId,
        levelId: null,
      },
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(
    await screen.findByLabelText('安静拼图 作品信息', undefined, {
      timeout: 3000,
    }),
  ).toBeTruthy();
});

test('home recommendation keeps cover while switching during a pending puzzle start', async () => {
  const user = userEvent.setup();
  const firstWork = {
    workId: 'puzzle-work-pending-next-1',
    profileId: 'puzzle-profile-pending-next-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-pending-next-1',
    authorDisplayName: '拼图作者',
    levelName: '雨港电路',
    summary: '第一张公开拼图仍在启动。',
    themeTags: ['雨港', '拼图'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T10:00:00.000Z',
    publishedAt: '2026-04-25T10:00:00.000Z',
    playCount: 47,
    likeCount: 1,
    publishReady: true,
  } satisfies PuzzleWorkSummary;
  const secondWork = {
    ...firstWork,
    workId: 'puzzle-work-pending-next-2',
    profileId: 'puzzle-profile-pending-next-2',
    ownerUserId: 'user-3',
    sourceSessionId: 'puzzle-session-pending-next-2',
    authorDisplayName: '贝壳作者',
    levelName: '贝壳潮汐',
    summary: '第二张公开拼图。',
    themeTags: ['贝壳', '拼图'],
    playCount: 1,
    likeCount: 0,
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
  } satisfies PuzzleWorkSummary;
  let resolveFirstRun!: (value: { run: PuzzleRunSnapshot }) => void;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [firstWork, secondWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
    item: profileId === secondWork.profileId ? secondWork : firstWork,
  }));
  vi.mocked(startPuzzleRun).mockImplementationOnce(
    (async () =>
      new Promise((resolve) => {
        resolveFirstRun = resolve;
      })) as typeof startPuzzleRun,
  );

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        canAccessProtectedData: false,
        openLoginModal: () => {},
        requireAuth: (action) => action(),
      })}
    />,
  );

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: firstWork.profileId,
        levelId: null,
      },
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
      }),
    );
  });

  await user.click(await screen.findByRole('button', { name: '下一个' }));

  expect(screen.queryByText('作品暂时无法进入，请稍后再试。')).toBeNull();
  expect(
    await screen.findByLabelText('贝壳潮汐 作品信息', undefined, {
      timeout: 3000,
    }),
  ).toBeTruthy();
  expect(startPuzzleRun).toHaveBeenCalledTimes(1);

  await act(async () => {
    resolveFirstRun({
      run: buildMockPuzzleRun(firstWork.profileId, '后端拼图关卡'),
    });
  });

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: secondWork.profileId,
        levelId: null,
      },
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
      }),
    );
  });
  expect(screen.queryByText('作品暂时无法进入，请稍后再试。')).toBeNull();
});

test('home recommendation puzzle next level uses unified recommend switching', async () => {
  const user = userEvent.setup();
  const entryWork = {
    workId: 'puzzle-work-public-guest-1',
    profileId: 'puzzle-profile-public-guest-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-public-guest-1',
    authorDisplayName: '拼图作者',
    levelName: '雨夜猫塔',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    likeCount: 0,
    publishReady: true,
    levels: [
      {
        levelId: 'puzzle-level-1',
        levelName: '雨夜猫塔',
        pictureDescription: '首关。',
        candidates: [],
        selectedCandidateId: null,
        coverImageSrc: null,
        coverAssetId: null,
        generationStatus: 'ready',
      },
      {
        levelId: 'puzzle-level-2',
        levelName: '星桥机关',
        pictureDescription: '同作品第二关。',
        candidates: [],
        selectedCandidateId: null,
        coverImageSrc: null,
        coverAssetId: null,
        generationStatus: 'ready',
      },
    ],
  } satisfies PuzzleWorkSummary;
  const nextRecommendWork = {
    ...entryWork,
    workId: 'puzzle-work-public-guest-2',
    profileId: 'puzzle-profile-public-guest-2',
    levelName: '风塔试炼',
    summary: '另一套推荐拼图。',
    levels: [
      {
        levelId: 'next-recommend-level-1',
        levelName: '风塔试炼',
        pictureDescription: '推荐队列下一张拼图。',
        candidates: [],
        selectedCandidateId: null,
        coverImageSrc: null,
        coverAssetId: null,
        generationStatus: 'ready',
      },
    ],
  } satisfies PuzzleWorkSummary;
  const clearedRun = buildClearedPuzzleRun({
    runId: 'run-puzzle-profile-public-guest-1',
    entryProfileId: entryWork.profileId,
    profileId: entryWork.profileId,
    levelName: entryWork.levelName,
    levelIndex: 1,
    elapsedMs: 18_000,
  });
  const clearedRunWithSameWorkNext: PuzzleRunSnapshot = {
    ...clearedRun,
    recommendedNextProfileId: entryWork.profileId,
    nextLevelMode: 'sameWork',
    nextLevelProfileId: entryWork.profileId,
    nextLevelId: 'puzzle-level-2',
    recommendedNextWorks: [],
  };
  const startedRun = buildMockPuzzleRun(
    entryWork.profileId,
    entryWork.levelName,
  );
  const nextRecommendRun = buildMockPuzzleRun(
    nextRecommendWork.profileId,
    nextRecommendWork.levelName,
  );

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [entryWork, nextRecommendWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
    item:
      profileId === nextRecommendWork.profileId ? nextRecommendWork : entryWork,
  }));
  vi.mocked(startPuzzleRun).mockImplementation(async (payload) => {
    const run =
      payload.profileId === nextRecommendWork.profileId
        ? nextRecommendRun
        : startedRun;
    return {
      run: {
        ...run,
        currentLevel: {
          ...run.currentLevel!,
          startedAtMs: Date.now(),
        },
      },
    };
  });
  vi.mocked(submitPuzzleLeaderboard).mockResolvedValue({
    run: clearedRunWithSameWorkNext,
  });
  vi.mocked(swapLocalPuzzlePieces).mockReturnValue(clearedRun);

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: entryWork.profileId,
        levelId: null,
      },
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  await waitFor(() => {
    expect(screen.getByTestId('puzzle-board')).toBeTruthy();
  });

  await user.click(document.querySelector('[data-piece-id="piece-0"]')!);
  await user.click(document.querySelector('[data-piece-id="piece-1"]')!);

  const dialog = await screen.findByRole(
    'dialog',
    { name: '通关完成' },
    { timeout: 3000 },
  );
  await user.click(within(dialog).getByRole('button', { name: '下一关' }));

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: nextRecommendWork.profileId,
        levelId: null,
      },
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(advancePuzzleNextLevel).not.toHaveBeenCalled();
  expect(screen.getByTestId('puzzle-board')).toBeTruthy();
  expect(screen.queryByText('加载中...')).toBeNull();
  expect(
    await screen.findByLabelText('风塔试炼 作品信息', undefined, {
      timeout: 3000,
    }),
  ).toBeTruthy();
  expect(startPuzzleRun).toHaveBeenCalledTimes(2);
});

test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => {
  const match3dCard: Match3DWorkSummary = {
    workId: 'match3d-work-card-1',
    profileId: 'match3d-profile-card-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'match3d-session-card-1',
    gameName: '果园抓大鹅',
    themeText: '果园',
    summary: '消除果园模型。',
    tags: ['果园', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 3,
    difficulty: 5,
    publicationStatus: 'published',
    playCount: 3,
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    generatedItemAssets: [],
  };
  const match3dDetail: Match3DWorkSummary = {
    ...match3dCard,
    generatedItemAssets: [
      {
        itemId: 'match3d-item-1',
        itemName: '草莓',
        imageSrc:
          '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        imageObjectKey:
          'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        modelSrc: null,
        modelObjectKey:
          'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
        modelFileName: 'strawberry.glb',
        taskUuid: 'task-strawberry',
        subscriptionKey: 'sub-strawberry',
        status: 'model_ready',
        error: null,
      },
    ],
  };

  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [match3dCard],
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: match3dDetail,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dCard.profileId),
  });

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
      'match3d-profile-card-1',
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  await waitFor(() => {
    expect(
      screen.getByTestId('match3d-runtime-generated-model-count'),
    ).toHaveProperty('textContent', '1');
  });
});

test('logged out home recommendation Match3D runtime skips protected detail and starts with guest auth', async () => {
  const match3dCard: Match3DWorkSummary = {
    workId: 'match3d-work-card-guest',
    profileId: 'match3d-profile-card-guest',
    ownerUserId: 'user-2',
    sourceSessionId: 'match3d-session-card-guest',
    gameName: '游客抓大鹅',
    themeText: '游客果园',
    summary: '游客可直接游玩。',
    tags: ['果园', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 3,
    difficulty: 5,
    publicationStatus: 'published',
    playCount: 3,
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    generatedItemAssets: [],
  };

  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [match3dCard],
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: match3dCard,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dCard.profileId),
  });

  render(<TestWrapper />);

  await waitFor(() => {
    expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
      'match3d-profile-card-guest',
      expect.objectContaining({
        runtimeGuestToken: 'runtime-guest-token',
        skipRefresh: true,
      }),
    );
  });
  expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
    'match3d-profile-card-guest',
  );
});

test('home recommendation Match3D runtime keeps image, music and UI assets without requiring models', async () => {
  const match3dCard: Match3DWorkSummary = {
    workId: 'match3d-work-card-image-only',
    profileId: 'match3d-profile-card-image-only',
    ownerUserId: 'user-2',
    sourceSessionId: 'match3d-session-card-image-only',
    gameName: '水果抓大鹅',
    themeText: '水果',
    summary: '消除水果模型。',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 3,
    difficulty: 5,
    publicationStatus: 'published',
    playCount: 3,
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    generatedItemAssets: [
      {
        itemId: 'match3d-item-1',
        itemName: '草莓',
        imageSrc:
          '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        imageObjectKey:
          'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        modelSrc: null,
        modelObjectKey: null,
        modelFileName: null,
        taskUuid: null,
        subscriptionKey: null,
        status: 'image_ready',
        error: null,
        backgroundMusic: {
          taskId: 'music-task-1',
          provider: 'vector-engine-suno',
          assetObjectId: 'asset-music-1',
          assetKind: 'match3d_background_music',
          audioSrc:
            '/generated-match3d-assets/session/profile/audio/background.mp3',
          prompt: '',
          title: '果园轻舞',
          updatedAt: '2026-05-12T10:00:00.000Z',
        },
        backgroundAsset: {
          prompt: '果园竖屏纯背景',
          imageSrc:
            '/generated-match3d-assets/session/profile/background/background.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/background/background.png',
          containerPrompt: '果园浅盘容器',
          containerImageSrc:
            '/generated-match3d-assets/session/profile/ui-container/container.png',
          containerImageObjectKey:
            'generated-match3d-assets/session/profile/ui-container/container.png',
          status: 'image_ready',
          error: null,
        },
      },
    ],
  };

  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [match3dCard],
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dCard.profileId),
  });

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(
      screen.getByTestId('match3d-runtime-generated-asset-count'),
    ).toHaveProperty('textContent', '1');
  });
  expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
    'match3d-profile-card-image-only',
  );
  expect(
    screen.getByTestId('match3d-runtime-background-music-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    screen.getByTestId('match3d-runtime-container-ui-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    screen.getByTestId('match3d-runtime-top-level-background-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
  ).toHaveProperty('textContent', '1');
});

test('home recommendation Match3D runtime passes top-level UI background assets', async () => {
  const match3dCard: Match3DWorkSummary = {
    workId: 'match3d-work-card-top-level-ui',
    profileId: 'match3d-profile-card-top-level-ui',
    ownerUserId: 'user-2',
    sourceSessionId: 'match3d-session-card-top-level-ui',
    gameName: '果园抓大鹅',
    themeText: '果园',
    summary: '消除果园素材。',
    tags: ['果园', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 3,
    difficulty: 5,
    publicationStatus: 'published',
    playCount: 3,
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    backgroundImageObjectKey:
      'generated-match3d-assets/session/profile/background/background.png',
    generatedBackgroundAsset: {
      prompt: '果园竖屏纯背景',
      imageSrc: null,
      imageObjectKey:
        'generated-match3d-assets/session/profile/background/background.png',
      containerPrompt: '果园浅盘容器',
      containerImageSrc: null,
      containerImageObjectKey:
        'generated-match3d-assets/session/profile/ui-container/container.png',
      status: 'image_ready',
      error: null,
    },
    generatedItemAssets: [
      {
        itemId: 'match3d-item-1',
        itemName: '草莓',
        imageSrc:
          '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        imageObjectKey:
          'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        modelSrc: null,
        modelObjectKey: null,
        modelFileName: null,
        taskUuid: null,
        subscriptionKey: null,
        status: 'image_ready',
        error: null,
      },
    ],
  };

  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [match3dCard],
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dCard.profileId),
  });

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(
      screen.getByTestId('match3d-runtime-top-level-background-count'),
    ).toHaveProperty('textContent', '1');
  });
  expect(
    screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
  ).toHaveProperty('textContent', '1');
  expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
    'match3d-profile-card-top-level-ui',
  );
});

test('home recommendation Match3D runtime reloads detail when card only has UI assets', async () => {
  const match3dCard: Match3DWorkSummary = {
    workId: 'match3d-work-card-ui-only',
    profileId: 'match3d-profile-card-ui-only',
    ownerUserId: 'user-2',
    sourceSessionId: 'match3d-session-card-ui-only',
    gameName: '水果抓大鹅',
    themeText: '水果',
    summary: '消除水果素材。',
    tags: ['水果', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 3,
    difficulty: 5,
    publicationStatus: 'published',
    playCount: 3,
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    generatedItemAssets: [
      {
        itemId: 'match3d-item-1',
        itemName: '草莓',
        imageSrc: null,
        imageObjectKey: null,
        imageViews: [],
        modelSrc: null,
        modelObjectKey: null,
        modelFileName: null,
        taskUuid: null,
        subscriptionKey: null,
        status: 'image_ready',
        error: null,
        backgroundAsset: {
          prompt: '果园竖屏纯背景',
          imageSrc:
            '/generated-match3d-assets/session/profile/background/background.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/background/background.png',
          containerPrompt: '果园浅盘容器',
          containerImageSrc:
            '/generated-match3d-assets/session/profile/ui-container/container.png',
          containerImageObjectKey:
            'generated-match3d-assets/session/profile/ui-container/container.png',
          status: 'image_ready',
          error: null,
        },
      },
    ],
  };
  const match3dDetail: Match3DWorkSummary = {
    ...match3dCard,
    generatedItemAssets: [
      {
        itemId: 'match3d-item-1',
        itemName: '草莓',
        imageSrc: null,
        imageObjectKey: null,
        imageViews: [
          {
            viewId: 'view-01',
            viewIndex: 1,
            imageSrc:
              '/generated-match3d-assets/session/profile/items/match3d-item-1-item/views/view-01.png',
            imageObjectKey: null,
          },
        ],
        modelSrc: null,
        modelObjectKey: null,
        modelFileName: null,
        taskUuid: null,
        subscriptionKey: null,
        status: 'image_ready',
        error: null,
      },
    ],
  };
  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [match3dCard],
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: match3dDetail,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dCard.profileId),
  });

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(getMatch3DWorkDetail).toHaveBeenCalledWith(
      'match3d-profile-card-ui-only',
    );
  });
  expect(
    await screen.findByTestId('match3d-runtime-generated-item-image-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    screen.getByTestId('match3d-runtime-top-level-background-count'),
  ).toHaveProperty('textContent', '1');
  expect(
    screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
  ).toHaveProperty('textContent', '1');
});

test('home recommendation surfaces start failure instead of staying in loading state', async () => {
  const publishedPuzzleWork = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'puzzle-session-public-1',
    authorDisplayName: '拼图作者',
    levelName: '星桥机关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [publishedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: publishedPuzzleWork,
  });
  vi.mocked(startPuzzleRun).mockRejectedValueOnce(
    new Error('启动拼图玩法失败'),
  );

  render(<TestWrapper withAuth />);

  expect(
    await screen.findByText('作品暂时无法进入，请稍后再试。'),
  ).toBeTruthy();
  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('home'))
        .queryByText('加载中...')
        ?.closest('.platform-recommend-runtime-panel'),
    ).toBeFalsy();
  });
});

test('published big fish works stay hidden from platform home and game category channel', async () => {
  const user = userEvent.setup();
  const publishedBigFishWork: BigFishWorkSummary = {
    workId: 'big-fish-work-public-1',
    sourceSessionId: 'big-fish-session-public-1',
    ownerUserId: 'user-2',
    authorDisplayName: '大鱼作者',
    title: '机械深海 大鱼吃小鱼',
    subtitle: '机械微生物吞并进化',
    summary: '从微光孢子一路吞并成长到深海巨鲲。',
    coverImageSrc: null,
    status: 'published',
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    levelCount: 8,
    levelMainImageReadyCount: 8,
    levelMotionReadyCount: 16,
    backgroundReady: true,
  };

  vi.mocked(listBigFishGallery).mockResolvedValue({
    items: [publishedBigFishWork],
  });

  render(<TestWrapper />);

  await waitFor(() => {
    expect(listBigFishGallery).not.toHaveBeenCalled();
  });
  expect(screen.queryByText('机械深海 大鱼吃小鱼')).toBeNull();

  await clickFirstButtonByName(user, '发现');
  await user.click(screen.getByRole('tab', { name: '分类' }));

  const discoverPanel = getPlatformTabPanel('category');
  expect(within(discoverPanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
  expect(
    within(discoverPanel).queryAllByRole('button', { name: /大鱼/u }).length,
  ).toBe(0);
});

test('published puzzle detail returns to the ranking platform tab', async () => {
  const user = userEvent.setup();
  const publishedPuzzleWork = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '星桥机关',
    summary: '旋转碎片并接通星桥机关。',
    themeTags: ['机关', '星桥'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 3,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [publishedPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: publishedPuzzleWork,
  });

  render(<TestWrapper withAuth />);

  await clickFirstButtonByName(user, '发现');
  await user.click(await screen.findByRole('tab', { name: '排行' }));
  await waitFor(() => {
    expect(document.getElementById('platform-tab-panel-category')).toBeTruthy();
  });
  await waitFor(() => {
    const rankingPanel = getPlatformTabPanel('category');
    expect(
      within(rankingPanel).getAllByText('星桥机关').length,
    ).toBeGreaterThan(0);
  });
  const rankingPanel = getPlatformTabPanel('category');

  await user.click(
    within(rankingPanel).getByRole('button', {
      name: /星桥机关/u,
    }),
  );
  await user.click(await screen.findByRole('button', { name: '启动' }));
  expect(await screen.findByTestId('puzzle-board')).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回上一页' }));
  await user.click(
    within(
      await screen.findByRole('dialog', {
        name: /体验不佳？\s*试试改造功能!/u,
      }),
    ).getByRole('button', { name: '保存并退出' }),
  );

  await waitFor(() => {
    expect(screen.getByRole('button', { name: '启动' })).toBeTruthy();
  });

  await user.click(screen.getByRole('button', { name: '返回' }));

  await waitFor(() => {
    const returnedRankingPanel = getPlatformTabPanel('category');
    expect(returnedRankingPanel.getAttribute('aria-hidden')).toBe('false');
    expect(
      within(returnedRankingPanel).getAllByText('星桥机关').length,
    ).toBeGreaterThan(0);
  });
});

test('restoring an agent workspace while logged out opens login modal before loading the protected session', async () => {
  const openLoginModal = vi.fn();

  window.history.replaceState(
    null,
    '',
    '/?customWorldSessionId=custom-world-agent-session-1',
  );

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        openLoginModal,
        requireAuth: vi.fn(),
      })}
    />,
  );

  await waitFor(() => {
    expect(openLoginModal).toHaveBeenCalledTimes(1);
  });

  expect(openLoginModal).toHaveBeenCalledWith(expect.any(Function));
  expect(getRpgCreationSession).not.toHaveBeenCalled();
});

test('restoring an agent workspace ignores a stored session owned by another user', async () => {
  window.sessionStorage.setItem(
    'genarrative.custom-world-agent-ui.v1',
    JSON.stringify({
      activeSessionId: 'custom-world-agent-session-other-user',
      activeOperationId: null,
      ownerUserId: 'user-other',
    }),
  );

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(
      window.sessionStorage.getItem('genarrative.custom-world-agent-ui.v1'),
    ).toBeNull();
  });

  expect(getRpgCreationSession).not.toHaveBeenCalled();
  expect(window.location.search).toBe('');
});

test('restoring an agent workspace ignores explicit session pointer without local owner after login', async () => {
  window.history.replaceState(
    null,
    '',
    '/?customWorldSessionId=custom-world-agent-session-legacy',
  );

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(window.location.search).toBe('');
  });

  expect(getRpgCreationSession).not.toHaveBeenCalled();
});

test('refreshing platform home ignores stored agent workspace pointer without explicit restore path', async () => {
  window.sessionStorage.setItem(
    'genarrative.custom-world-agent-ui.v1',
    JSON.stringify({
      activeSessionId: 'custom-world-agent-session-1',
      activeOperationId: null,
      ownerUserId: 'user-1',
    }),
  );

  render(<TestWrapper withAuth />);

  expect(await screen.findByRole('button', { name: '创作' })).toBeTruthy();
  expect(screen.queryByText(/Agent工作区/u)).toBeNull();
  expect(getRpgCreationSession).not.toHaveBeenCalled();
  expect(window.location.pathname).toBe('/');
});

test('refreshing RPG agent path restores stored agent workspace pointer', async () => {
  window.history.replaceState(null, '', '/creation/rpg/agent');
  window.sessionStorage.setItem(
    'genarrative.custom-world-agent-ui.v1',
    JSON.stringify({
      activeSessionId: 'custom-world-agent-session-1',
      activeOperationId: null,
      ownerUserId: 'user-1',
    }),
  );

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(getRpgCreationSession).toHaveBeenCalledWith(
      'custom-world-agent-session-1',
    );
  });
  expect(
    await screen.findByText('Agent工作区：custom-world-agent-session-1'),
  ).toBeTruthy();
});

test('direct jump hop result route shows recovery panel when no draft pointer exists', async () => {
  window.history.replaceState(null, '', '/creation/jump-hop/result');

  render(<TestWrapper withAuth />);

  expect(await screen.findByText('跳一跳草稿未恢复')).toBeTruthy();
  expect(screen.getByRole('button', { name: '返回创作' })).toBeTruthy();
  expect(jumpHopClient.getWorkDetail).not.toHaveBeenCalled();
  expect(jumpHopClient.getSession).not.toHaveBeenCalled();
});

test('direct jump hop result route restores work detail by profile id', async () => {
  const work = buildMockJumpHopWork({
    summary: {
      runtimeKind: 'jump-hop',
      workId: 'jump-hop-work-restore-1',
      profileId: 'jump-hop-profile-restore-1',
      ownerUserId: 'user-1',
      sourceSessionId: null,
      themeText: '恢复后的云端跳台',
      workTitle: '恢复后的云端跳台',
      workDescription: '从 profileId 回读完整跳一跳结果。',
      themeTags: ['云朵'],
      difficulty: 'standard',
      stylePreset: 'paper-toy',
      coverImageSrc: null,
      publicationStatus: 'draft',
      playCount: 0,
      updatedAt: '2026-05-30T10:00:00.000Z',
      publishedAt: null,
      publishReady: true,
      generationStatus: 'ready',
    },
  });
  vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValueOnce({
    item: work,
  } satisfies JumpHopWorkDetailResponse);
  window.history.replaceState(
    null,
    '',
    '/creation/jump-hop/result?profileId=jump-hop-profile-restore-1',
  );

  render(<TestWrapper withAuth />);

  expect(await screen.findByText('恢复后的云端跳台')).toBeTruthy();
  expect(screen.queryByText('跳一跳草稿未恢复')).toBeNull();
  expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
    'jump-hop-profile-restore-1',
    { audience: 'creation' },
  );
  expect(jumpHopClient.getSession).not.toHaveBeenCalled();
});

test('completed unpublished jump hop draft opens result page without starting runtime', async () => {
  const user = userEvent.setup();
  const work = buildMockJumpHopWork({
    summary: {
      runtimeKind: 'jump-hop',
      workId: 'jump-hop-work-draft-ready-1',
      profileId: 'jump-hop-profile-draft-ready-1',
      ownerUserId: 'user-1',
      sourceSessionId: 'jump-hop-session-draft-ready-1',
      themeText: '未发布跳一跳草稿',
      workTitle: '未发布跳一跳草稿',
      workDescription: '已经生成完成，但还没有发布。',
      themeTags: ['草稿'],
      difficulty: 'standard',
      stylePreset: 'paper-toy',
      coverImageSrc: null,
      publicationStatus: 'draft',
      playCount: 0,
      updatedAt: '2026-05-30T10:00:00.000Z',
      publishedAt: null,
      publishReady: true,
      generationStatus: 'ready',
    },
  });
  vi.mocked(jumpHopClient.listWorks).mockResolvedValue({
    items: [work.summary],
  });
  vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValueOnce({
    item: work,
  } satisfies JumpHopWorkDetailResponse);
  vi.mocked(fetchCreationEntryConfig).mockResolvedValueOnce({
    ...testCreationEntryConfig,
    creationTypes: [
      ...testCreationEntryConfig.creationTypes,
      {
        id: 'jump-hop',
        title: '跳一跳',
        subtitle: '主题驱动平台跳跃',
        badge: '可创建',
        imageSrc: '/creation-type-references/jump-hop.webp',
        visible: true,
        open: true,
        sortOrder: 55,
        categoryId: 'recommended',
        categoryLabel: '热门推荐',
        categorySortOrder: 20,
        updatedAtMicros: 1,
      },
    ],
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  const draftPanel = getPlatformTabPanel('saves');
  await user.click(
    await within(draftPanel).findByRole('button', {
      name: /继续创作《未发布跳一跳草稿》/u,
    }),
  );

  expect(await screen.findByText('未发布跳一跳草稿')).toBeTruthy();
  expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
    'jump-hop-profile-draft-ready-1',
    { audience: 'creation' },
  );
  expect(jumpHopClient.startRun).not.toHaveBeenCalled();
  expect(window.location.pathname).toBe('/creation/jump-hop/result');
  expect(window.location.search).toContain(
    'profileId=jump-hop-profile-draft-ready-1',
  );
});

test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
  const user = userEvent.setup();

  vi.mocked(createPuzzleAgentSession).mockRejectedValueOnce(
    new ApiClientError({
      message: '缺少 Authorization Bearer Token',
      status: 401,
      code: 'UNAUTHORIZED',
    }),
  );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await screen.findByText(/拼图工作区：/u);
  const generateButton = screen.getByRole('button', { name: '生成草稿' });

  expect((generateButton as HTMLButtonElement).disabled).toBe(false);
  await user.click(generateButton);

  expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1);
  expect(createCreativeAgentSession).not.toHaveBeenCalled();
  expect(
    (await screen.findAllByText('当前登录状态已失效，请重新登录后继续。'))
      .length,
  ).toBeTruthy();
  expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
});

test('create tab does not render legacy gameplay creation entries', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);

  expect(screen.queryByText('选择创作类型')).toBeNull();
  expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
  expect(createBigFishCreationSession).not.toHaveBeenCalled();
});

test('embedded puzzle form timeout exits busy state and shows a readable error', async () => {
  const user = userEvent.setup();

  vi.mocked(createPuzzleAgentSession).mockRejectedValueOnce(
    Object.assign(new Error('请求超时：15000ms'), {
      name: 'TimeoutError',
    }),
  );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await screen.findByText(/拼图工作区：/u);

  const button = screen.getByRole('button', { name: '生成草稿' });
  await user.click(button);

  await waitFor(() => {
    expect(
      screen.getAllByText(
        '开启拼图创作工作台超时，请确认运行时后端已启动后重试。',
      ).length,
    ).toBeGreaterThan(0);
  });
  expect(button as HTMLButtonElement).toHaveProperty('disabled', false);
  expect(createCreativeAgentSession).not.toHaveBeenCalled();
  expect(streamCreativeAgentMessage).not.toHaveBeenCalled();
});

test('match3d creation tab stays usable even when public galleries fail', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgEntryWorldGallery).mockRejectedValueOnce(
    new Error('读取作品广场失败'),
  );
  vi.mocked(listMatch3DGallery).mockRejectedValueOnce(
    new Error('读取抓大鹅广场失败'),
  );

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  expect(screen.queryByText('读取作品广场失败')).toBeNull();
  expect(screen.queryByText('读取抓大鹅广场失败')).toBeNull();
  expect(await findCreationTypeButton('抓大鹅')).toBeTruthy();
  expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
});

test('puzzle draft result back button returns to draft hub when opened from shelf', async () => {
  const user = userEvent.setup();

  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [
      {
        workId: 'puzzle-work-session-1',
        profileId: 'puzzle-profile-session-1',
        ownerUserId: 'user-1',
        sourceSessionId: 'puzzle-session-1',
        authorDisplayName: '测试玩家',
        levelName: '雨夜猫塔',
        summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
        themeTags: ['雨夜', '猫咪', '遗迹'],
        coverImageSrc: null,
        coverAssetId: null,
        publicationStatus: 'draft',
        updatedAt: '2026-04-22T12:10:00.000Z',
        publishedAt: null,
        playCount: 0,
        likeCount: 0,
        publishReady: false,
      },
    ],
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);

  expect(await screen.findByRole('button', { name: /继续创作/u })).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: /继续创作/u }));

  await waitFor(() => {
    expect(getPuzzleAgentSession).toHaveBeenCalledWith('puzzle-session-1');
  });

  expect(await screen.findByText('拼图结果页')).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '返回' }));

  const draftPanel = await findPlatformTabPanel('saves');
  await waitFor(() => {
    expect(draftPanel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(
    within(draftPanel).getByRole('tablist', { name: '作品筛选' }),
  ).toBeTruthy();
  expect(within(draftPanel).getByText('雨夜猫塔')).toBeTruthy();
  expect(getPlatformTabPanel('create').getAttribute('aria-hidden')).toBe(
    'true',
  );
  expect(screen.queryByText('拼图工作区：missing-session')).toBeNull();
  expect(
    screen.queryByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
  ).toBeNull();
  expect(screen.queryByText('拼图结果页')).toBeNull();
});

test('persisted generating puzzle draft opens generation progress after refresh', async () => {
  const user = userEvent.setup();

  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [
      {
        workId: 'puzzle-work-session-generating',
        profileId: 'puzzle-profile-session-generating',
        ownerUserId: 'user-1',
        sourceSessionId: 'puzzle-session-generating',
        authorDisplayName: '测试玩家',
        workTitle: '生成中拼图',
        workDescription: '刷新后仍应回到生成面板。',
        levelName: '生成中拼图',
        summary: '刷新后仍应回到生成面板。',
        themeTags: ['雨夜'],
        coverImageSrc: null,
        coverAssetId: null,
        publicationStatus: 'draft',
        updatedAt: '2026-05-18T12:00:00.000Z',
        publishedAt: null,
        playCount: 0,
        remixCount: 0,
        likeCount: 0,
        publishReady: false,
        generationStatus: 'generating',
      },
    ],
  });
  const persistedGeneratingPuzzleSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-session-generating',
    stage: 'collecting_anchors',
    progressPercent: 88,
    lastAssistantReply: '正在生成拼图草稿。',
    updatedAt: '2026-05-18T12:00:00.000Z',
  });
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: persistedGeneratingPuzzleSession,
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /继续创作/u }));

  await waitFor(() => {
    expect(getPuzzleAgentSession).toHaveBeenCalledWith(
      'puzzle-session-generating',
    );
  });
  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();
  const restoredProgressValue = Number(
    screen
      .getByRole('progressbar', { name: '拼图图片生成进度' })
      .getAttribute('aria-valuenow'),
  );
  expect(restoredProgressValue).toBeGreaterThan(0);
  expect(restoredProgressValue).toBeLessThan(100);
  expect(screen.getByText(`${restoredProgressValue}%`)).toBeTruthy();
  expect(screen.queryByText('拼图结果页')).toBeNull();
});

test('persisted generating puzzle draft keeps session polling on the same session', async () => {
  const user = userEvent.setup();

  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [
      {
        workId: 'puzzle-work-session-generating',
        profileId: 'puzzle-profile-session-generating',
        ownerUserId: 'user-1',
        sourceSessionId: 'puzzle-session-generating',
        authorDisplayName: '测试玩家',
        workTitle: '生成中拼图',
        workDescription: '刷新后仍应回到生成面板。',
        levelName: '生成中拼图',
        summary: '刷新后仍应回到生成面板。',
        themeTags: ['雨夜'],
        coverImageSrc: null,
        coverAssetId: null,
        publicationStatus: 'draft',
        updatedAt: '2026-05-18T12:00:00.000Z',
        publishedAt: null,
        playCount: 0,
        remixCount: 0,
        likeCount: 0,
        publishReady: false,
        generationStatus: 'generating',
      },
    ],
  });
  const persistedGeneratingPuzzleSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-session-generating',
    stage: 'collecting_anchors',
    progressPercent: 88,
    lastAssistantReply: '正在生成拼图草稿。',
    updatedAt: '2026-05-18T12:00:00.000Z',
  });
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: persistedGeneratingPuzzleSession,
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /继续创作/u }));

  await waitFor(() => {
    expect(getPuzzleAgentSession).toHaveBeenCalledTimes(2);
  });

  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();

  await act(async () => {
    await new Promise((resolve) => window.setTimeout(resolve, 120));
  });

  expect(getPuzzleAgentSession).toHaveBeenCalledTimes(2);
});

test('puzzle compile timeout shows failure dialog when reread session is still generating', async () => {
  const user = userEvent.setup();
  const runningSession = buildMockPuzzleAgentSession({
    sessionId: 'puzzle-session-timeout',
    draft: null,
    stage: 'collecting_anchors',
    progressPercent: 88,
    lastAssistantReply: '正在生成拼图草稿。',
  });
  vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
    session: runningSession,
  });
  vi.mocked(executePuzzleAgentAction).mockRejectedValueOnce(
    Object.assign(new Error('请求超时：1800000ms'), {
      name: 'TimeoutError',
    }),
  );
  vi.mocked(getPuzzleAgentSession).mockResolvedValue({
    session: runningSession,
  });

  render(<TestWrapper withAuth />);

  await openCreateTemplateHub(user);
  await user.click(await findCreationTypeButton('拼图'));
  await user.click(await screen.findByRole('button', { name: '生成草稿' }));

  const dialog = await screen.findByRole('dialog', { name: '发生错误' });
  expect(
    within(dialog).getByText('拼图草稿 puzzle-session-timeout'),
  ).toBeTruthy();
  expect(
    within(dialog).getByText(
      '拼图共创操作超时，请确认运行时后端已启动后重试。',
    ),
  ).toBeTruthy();
  expect(within(dialog).getByRole('button', { name: '复制报错' })).toBeTruthy();
  expect(
    await screen.findByRole('progressbar', {
      name: '拼图图片生成进度',
    }),
  ).toBeTruthy();
});

test('published puzzle work card restores its source session for editing', async () => {
  const user = userEvent.setup();

  vi.mocked(listPuzzleWorks).mockResolvedValue({
    items: [
      {
        workId: 'puzzle-work-session-1',
        profileId: 'puzzle-profile-session-1',
        ownerUserId: 'user-1',
        sourceSessionId: 'puzzle-session-1',
        authorDisplayName: '测试玩家',
        levelName: '雨夜猫塔',
        summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
        themeTags: ['雨夜', '猫咪', '遗迹'],
        coverImageSrc: null,
        coverAssetId: null,
        publicationStatus: 'published',
        updatedAt: '2026-04-25T12:10:00.000Z',
        publishedAt: '2026-04-25T12:10:00.000Z',
        playCount: 8,
        likeCount: 0,
        publishReady: true,
      },
    ],
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);

  expect(await screen.findByRole('button', { name: /继续创作/u })).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: /继续创作/u }));

  await waitFor(() => {
    expect(getPuzzleAgentSession).toHaveBeenCalledWith('puzzle-session-1');
  });

  expect(getPuzzleGalleryDetail).not.toHaveBeenCalledWith(
    'puzzle-profile-session-1',
  );
  expect(await screen.findByText('拼图结果页')).toBeTruthy();
  expect(screen.getByDisplayValue('雨夜猫塔')).toBeTruthy();
});

test('first launch hides puzzle onboarding by default', async () => {
  window.localStorage.removeItem(
    'genarrative.puzzle-onboarding.first-visit.v1',
  );

  render(
    <TestWrapper
      authValue={createAuthValue({
        user: null,
        canAccessProtectedData: false,
        openLoginModal: () => {},
        requireAuth: () => {},
      })}
    />,
  );

  await waitFor(() => {
    expect(screen.queryByText('待定待定待定')).toBeNull();
  });
  expect(screen.queryByPlaceholderText('把你的梦讲给我听吧')).toBeNull();
  expect(
    window.localStorage.getItem('genarrative.puzzle-onboarding.first-visit.v1'),
  ).toBeNull();
  expect(generatePuzzleOnboardingWork).not.toHaveBeenCalled();
});

test('formal puzzle runtime uses frontend move merge logic and backend leaderboard next level', async () => {
  const user = userEvent.setup();
  const clearedFirstLevel = buildClearedPuzzleRun({
    runId: 'run-puzzle-profile-public-1',
    entryProfileId: 'puzzle-profile-public-1',
    profileId: 'puzzle-profile-public-1',
    levelName: '雨夜猫塔',
    levelIndex: 1,
    elapsedMs: 18_000,
  });
  const clearedFirstLevelWithNext = {
    ...clearedFirstLevel,
    recommendedNextProfileId: 'puzzle-profile-public-1',
    nextLevelMode: 'sameWork' as const,
    nextLevelProfileId: 'puzzle-profile-public-1',
    nextLevelId: 'puzzle-level-2',
    recommendedNextWorks: [],
  };
  const leaderboardEntries = [
    {
      rank: 1,
      nickname: '测试玩家',
      elapsedMs: 18_000,
      isCurrentPlayer: true,
    },
  ];
  const backendLeaderboardRun = {
    ...clearedFirstLevelWithNext,
    leaderboardEntries,
    currentLevel: {
      ...clearedFirstLevelWithNext.currentLevel!,
      leaderboardEntries,
    },
  };
  const backendSecondLevel = {
    ...buildMockPuzzleRun('puzzle-profile-public-1', '星桥机关'),
    runId: clearedFirstLevel.runId,
    entryProfileId: clearedFirstLevel.entryProfileId,
    currentLevelIndex: 2,
    currentLevel: {
      ...buildMockPuzzleRun('puzzle-profile-public-1', '星桥机关')
        .currentLevel!,
      runId: clearedFirstLevel.runId,
      levelIndex: 2,
      levelId: 'puzzle-level-2',
      startedAtMs: Date.now(),
    },
  };
  const backendStartedRun = buildMockPuzzleRun(
    'puzzle-profile-public-1',
    '雨夜猫塔',
  );
  vi.mocked(startPuzzleRun).mockResolvedValue({
    run: {
      ...backendStartedRun,
      currentLevel: {
        ...backendStartedRun.currentLevel!,
        startedAtMs: Date.now(),
      },
    },
  });
  vi.mocked(submitPuzzleLeaderboard).mockResolvedValue({
    run: backendLeaderboardRun,
  });
  vi.mocked(advancePuzzleNextLevel).mockResolvedValue({
    run: backendSecondLevel,
  });
  vi.mocked(dragLocalPuzzlePiece).mockReturnValue(clearedFirstLevel);
  vi.mocked(swapLocalPuzzlePieces).mockReturnValue(clearedFirstLevel);

  const puzzleWork: PuzzleWorkSummary = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '雨夜猫塔',
    summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
    themeTags: ['雨夜', '猫咪', '遗迹'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T12:10:00.000Z',
    publishedAt: '2026-04-25T12:10:00.000Z',
    playCount: 8,
    likeCount: 0,
    publishReady: true,
    levels: [
      {
        levelId: 'puzzle-level-1',
        levelName: '雨夜猫塔',
        pictureDescription: '雨夜猫塔首关。',
        candidates: [],
        selectedCandidateId: null,
        coverImageSrc: null,
        coverAssetId: null,
        generationStatus: 'ready',
      },
      {
        levelId: 'puzzle-level-2',
        levelName: '星桥机关',
        pictureDescription: '星桥机关第二关。',
        candidates: [],
        selectedCandidateId: null,
        coverImageSrc: null,
        coverAssetId: null,
        generationStatus: 'ready',
      },
    ],
  };

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [puzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: puzzleWork,
  });
  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'PZ-EPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  await user.click(await screen.findByRole('button', { name: '启动' }));
  await waitFor(() => {
    expect(screen.getByTestId('puzzle-board')).toBeTruthy();
  });
  expect(startPuzzleRun).toHaveBeenCalledWith(
    {
      profileId: 'puzzle-profile-public-1',
      levelId: null,
    },
    ISOLATED_RUNTIME_AUTH_OPTIONS,
  );
  vi.mocked(listProfileSaveArchives).mockClear();
  vi.mocked(listProfileSaveArchives).mockRejectedValueOnce(
    new Error('后台存档刷新 401'),
  );

  await user.click(document.querySelector('[data-piece-id="piece-0"]')!);
  await user.click(document.querySelector('[data-piece-id="piece-1"]')!);

  await waitFor(() => {
    expect(swapLocalPuzzlePieces).toHaveBeenCalled();
  });
  expect(swapPuzzlePieces).not.toHaveBeenCalled();
  expect(dragPuzzlePieceOrGroup).not.toHaveBeenCalled();
  await waitFor(() => {
    expect(submitPuzzleLeaderboard).toHaveBeenCalledWith(
      clearedFirstLevel.runId,
      {
        profileId: 'puzzle-profile-public-1',
        gridSize: 3,
        elapsedMs: 18_000,
        nickname: '测试玩家',
      },
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });

  const dialog = await screen.findByRole(
    'dialog',
    { name: '通关完成' },
    { timeout: 3000 },
  );
  expect(dialog).toBeTruthy();
  expect(screen.getByText('测试玩家')).toBeTruthy();
  expect(listProfileSaveArchives).toHaveBeenCalledWith(
    ISOLATED_RUNTIME_AUTH_OPTIONS,
  );

  await user.click(within(dialog).getByRole('button', { name: '下一关' }));

  await waitFor(() => {
    expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
      clearedFirstLevel.runId,
      {},
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(
    (
      await screen.findAllByText('星桥机关', undefined, {
        timeout: 3000,
      })
    ).length,
  ).toBeGreaterThan(0);
});

test('formal puzzle similar work keeps current run level progression', async () => {
  const user = userEvent.setup();
  const clearedThirdLevel = buildClearedPuzzleRun({
    runId: 'run-puzzle-profile-public-1',
    entryProfileId: 'puzzle-profile-public-1',
    profileId: 'puzzle-profile-public-1',
    levelName: '雨夜猫塔',
    levelIndex: 3,
    elapsedMs: 18_000,
    recommendedNextProfileId: 'puzzle-profile-similar-2',
  });
  const clearedThirdLevelWithCandidates: PuzzleRunSnapshot = {
    ...clearedThirdLevel,
    nextLevelMode: 'similarWorks',
    nextLevelProfileId: 'puzzle-profile-similar-1',
    nextLevelId: null,
    recommendedNextWorks: [
      {
        profileId: 'puzzle-profile-similar-1',
        levelName: '雾海遗迹',
        authorDisplayName: '星桥旅人',
        themeTags: ['奇幻', '遗迹'],
        coverImageSrc: null,
        similarityScore: 0.91,
      },
      {
        profileId: 'puzzle-profile-similar-2',
        levelName: '风塔试炼',
        authorDisplayName: '晨风',
        themeTags: ['奇幻', '机关'],
        coverImageSrc: null,
        similarityScore: 0.84,
      },
    ],
  };
  const similarFourthLevel = {
    ...buildMockPuzzleRun('puzzle-profile-similar-2', '风塔试炼'),
    runId: clearedThirdLevel.runId,
    entryProfileId: clearedThirdLevel.entryProfileId,
    currentLevelIndex: 4,
    currentGridSize: 5 as const,
    playedProfileIds: ['puzzle-profile-public-1', 'puzzle-profile-similar-2'],
    currentLevel: {
      ...buildMockPuzzleRun('puzzle-profile-similar-2', '风塔试炼')
        .currentLevel!,
      runId: clearedThirdLevel.runId,
      levelIndex: 4,
      levelId: 'similar-level-1',
      gridSize: 5 as const,
      timeLimitMs: 210_000,
      remainingMs: 210_000,
      startedAtMs: Date.now(),
      board: {
        rows: 5,
        cols: 5,
        selectedPieceId: null,
        allTilesResolved: false,
        mergedGroups: [],
        pieces: Array.from({ length: 25 }, (_, index) => ({
          pieceId: `piece-${index}`,
          correctRow: Math.floor(index / 5),
          correctCol: index % 5,
          currentRow: Math.floor(index / 5),
          currentCol: index % 5,
          mergedGroupId: null,
        })),
      },
    },
  };
  const backendStartedRun = buildMockPuzzleRun(
    'puzzle-profile-public-1',
    '雨夜猫塔',
  );
  vi.mocked(startPuzzleRun).mockResolvedValue({
    run: {
      ...backendStartedRun,
      currentLevel: {
        ...backendStartedRun.currentLevel!,
        startedAtMs: Date.now(),
      },
    },
  });
  vi.mocked(submitPuzzleLeaderboard).mockResolvedValue({
    run: clearedThirdLevelWithCandidates,
  });
  vi.mocked(advancePuzzleNextLevel).mockResolvedValue({
    run: similarFourthLevel,
  });
  vi.mocked(dragLocalPuzzlePiece).mockReturnValue(clearedThirdLevel);
  vi.mocked(swapLocalPuzzlePieces).mockReturnValue(clearedThirdLevel);

  const entryWork: PuzzleWorkSummary = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '雨夜猫塔',
    summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
    themeTags: ['雨夜', '猫咪', '遗迹'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T12:10:00.000Z',
    publishedAt: '2026-04-25T12:10:00.000Z',
    playCount: 8,
    likeCount: 0,
    publishReady: true,
  };
  const similarWork: PuzzleWorkSummary = {
    ...entryWork,
    workId: 'puzzle-work-similar-2',
    profileId: 'puzzle-profile-similar-2',
    levelName: '风塔试炼',
    summary: '另一套奇幻机关拼图。',
  };

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [entryWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
    item: profileId === similarWork.profileId ? similarWork : entryWork,
  }));
  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'PZ-EPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  await user.click(await screen.findByRole('button', { name: '启动' }));
  await waitFor(() => {
    expect(screen.getByTestId('puzzle-board')).toBeTruthy();
  });
  vi.mocked(startPuzzleRun).mockClear();

  await user.click(document.querySelector('[data-piece-id="piece-0"]')!);
  await user.click(document.querySelector('[data-piece-id="piece-1"]')!);

  const dialog = await screen.findByRole(
    'dialog',
    { name: '通关完成' },
    { timeout: 3000 },
  );
  await user.click(within(dialog).getByRole('button', { name: /风塔试炼/u }));

  await waitFor(() => {
    expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
      clearedThirdLevel.runId,
      { targetProfileId: 'puzzle-profile-similar-2' },
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(startPuzzleRun).not.toHaveBeenCalled();
  expect(await screen.findByText('第 4 关')).toBeTruthy();
  await waitFor(() => {
    expect(document.querySelectorAll('[data-piece-id]').length).toBe(25);
  });
});

test('first puzzle runtime back click can open remix result page', async () => {
  const user = userEvent.setup();
  const puzzleWork: PuzzleWorkSummary = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '雨夜猫塔',
    summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
    themeTags: ['雨夜', '猫咪', '遗迹'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T12:10:00.000Z',
    publishedAt: '2026-04-25T12:10:00.000Z',
    playCount: 8,
    remixCount: 0,
    likeCount: 0,
    publishReady: true,
  };
  const anchorPack = buildPuzzleAnchorPack();
  const remixDraft: PuzzleResultDraft = {
    workTitle: '改造后的雨夜猫塔',
    workDescription: '准备改造的拼图草稿。',
    levelName: '改造后的雨夜猫塔',
    summary: '一只猫站在雨夜塔顶。',
    themeTags: ['雨夜', '猫咪', '塔'],
    forbiddenDirectives: [],
    creatorIntent: null,
    anchorPack,
    candidates: [],
    selectedCandidateId: null,
    coverImageSrc: null,
    coverAssetId: null,
    generationStatus: 'idle',
    levels: [],
    metadata: null,
  };
  const remixSession: PuzzleAgentSessionSnapshot = {
    sessionId: 'puzzle-session-remix-1',
    currentTurn: 1,
    progressPercent: 100,
    stage: 'ready_to_publish',
    anchorPack,
    draft: remixDraft,
    messages: [],
    lastAssistantReply: null,
    publishedProfileId: null,
    suggestedActions: [],
    resultPreview: null,
    updatedAt: '2026-04-25T12:12:00.000Z',
  };

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [puzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: puzzleWork,
  });
  vi.mocked(remixPuzzleGalleryWork).mockResolvedValue({
    session: remixSession,
  });

  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'PZ-EPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));
  await user.click(await screen.findByRole('button', { name: '启动' }));
  expect(await screen.findByTestId('puzzle-board')).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: '返回上一页' }));

  const dialog = await screen.findByRole('dialog', {
    name: /体验不佳？\s*试试改造功能!/u,
  });
  await user.click(within(dialog).getByRole('button', { name: '作品改造' }));

  await waitFor(() => {
    expect(remixPuzzleGalleryWork).toHaveBeenCalledWith(
      'puzzle-profile-public-1',
    );
  });
  expect(await screen.findByText('拼图结果页')).toBeTruthy();
  expect(screen.getByDisplayValue('改造后的雨夜猫塔')).toBeTruthy();
});

test('recommend puzzle remix return restarts recommendation instead of stale loading run', async () => {
  const user = userEvent.setup();
  const puzzleWork: PuzzleWorkSummary = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '雨夜猫塔',
    summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
    themeTags: ['雨夜', '猫咪', '遗迹'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T12:10:00.000Z',
    publishedAt: '2026-04-25T12:10:00.000Z',
    playCount: 8,
    remixCount: 0,
    likeCount: 0,
    publishReady: true,
  };
  const anchorPack = buildPuzzleAnchorPack();
  const remixDraft: PuzzleResultDraft = {
    workTitle: '改造后的雨夜猫塔',
    workDescription: '准备改造的拼图草稿。',
    levelName: '改造后的雨夜猫塔',
    summary: '一只猫站在雨夜塔顶。',
    themeTags: ['雨夜', '猫咪', '塔'],
    forbiddenDirectives: [],
    creatorIntent: null,
    anchorPack,
    candidates: [],
    selectedCandidateId: null,
    coverImageSrc: null,
    coverAssetId: null,
    generationStatus: 'idle',
    levels: [],
    metadata: null,
  };
  const remixSession: PuzzleAgentSessionSnapshot = {
    sessionId: 'puzzle-session-remix-1',
    currentTurn: 1,
    progressPercent: 100,
    stage: 'ready_to_publish',
    anchorPack,
    draft: remixDraft,
    messages: [],
    lastAssistantReply: null,
    publishedProfileId: null,
    suggestedActions: [],
    resultPreview: null,
    updatedAt: '2026-04-25T12:12:00.000Z',
  };

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [puzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: puzzleWork,
  });
  vi.mocked(remixPuzzleGalleryWork).mockResolvedValue({
    session: remixSession,
  });

  render(<TestWrapper withAuth />);

  await waitFor(() => {
    expect(screen.getByTestId('puzzle-board')).toBeTruthy();
  });

  await user.click(screen.getByRole('button', { name: '改造 0' }));

  expect(await screen.findByText('拼图结果页')).toBeTruthy();
  expect(screen.getByDisplayValue('改造后的雨夜猫塔')).toBeTruthy();

  vi.mocked(startPuzzleRun).mockClear();
  await user.click(screen.getByRole('button', { name: '返回' }));
  await clickFirstButtonByName(user, '推荐');

  await waitFor(() => {
    expect(startPuzzleRun).toHaveBeenCalledWith(
      {
        profileId: 'puzzle-profile-public-1',
        levelId: null,
      },
      expect.objectContaining({
        authImpact: 'local',
      }),
    );
  });
  expect(screen.queryByText('正在进入拼图关卡')).toBeNull();
});

test('public code search opens a published puzzle by PZ code', async () => {
  const user = userEvent.setup();
  const puzzleWork: PuzzleWorkSummary = {
    workId: 'puzzle-work-public-1',
    profileId: 'puzzle-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '雨夜猫塔',
    summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
    themeTags: ['雨夜', '猫咪', '遗迹'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T12:10:00.000Z',
    publishedAt: '2026-04-25T12:10:00.000Z',
    playCount: 8,
    likeCount: 0,
    publishReady: true,
  };

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [puzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
    item: puzzleWork,
  });

  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'PZ-EPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  await waitFor(() => {
    expect(getPuzzleGalleryDetail).toHaveBeenCalledWith(
      'puzzle-profile-public-1',
    );
  });
  expect(await screen.findByText('详情')).toBeTruthy();
  expect(screen.getByText('雨夜猫塔')).toBeTruthy();
  expect(screen.getByRole('button', { name: '启动' })).toBeTruthy();
  expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
});

test('missing puzzle public detail returns to platform home', async () => {
  const user = userEvent.setup();
  const missingPuzzleWork = {
    workId: 'puzzle-work-missing-1',
    profileId: 'puzzle-profile-missing-1',
    ownerUserId: 'user-2',
    sourceSessionId: null,
    authorDisplayName: '拼图作者',
    levelName: '失效拼图',
    summary: '这个作品已经不可用。',
    themeTags: ['失效'],
    coverImageSrc: null,
    coverAssetId: null,
    publicationStatus: 'published',
    updatedAt: '2026-04-25T09:00:00.000Z',
    publishedAt: '2026-04-25T09:00:00.000Z',
    playCount: 1,
    remixCount: 0,
    likeCount: 0,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [missingPuzzleWork],
  });
  vi.mocked(getPuzzleGalleryDetail).mockRejectedValueOnce(
    new ApiClientError({
      message: '资源不存在',
      status: 404,
      code: 'NOT_FOUND',
    }),
  );

  render(<TestWrapper />);
  vi.mocked(startPuzzleRun).mockClear();
  await openDiscoverHub(user);

  const workCards = await screen.findAllByRole('button', { name: /失效拼图/u });
  await user.click(workCards[0]!);

  await waitFor(() => {
    expect(window.location.pathname).toBe('/');
  });
  expect(getPlatformTabPanel('home').getAttribute('aria-hidden')).toBe('false');
  expect(screen.queryByText('详情')).toBeNull();
  expect(screen.queryByText('资源不存在')).toBeNull();
});

test('direct missing public work detail shows unified dialog before returning home', async () => {
  const user = userEvent.setup();
  const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});

  window.history.replaceState(null, '', '/works/detail?work=PZ-7A7B18D9');
  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [],
  });

  render(<TestWrapper withAuth />);

  expect(await screen.findByText('正在读取作品详情...')).toBeTruthy();

  const dialog = await screen.findByRole('dialog', { name: '作品不可用' });
  expect(within(dialog).getByText('作品不存在或已下架，将返回首页。')).toBeTruthy();
  expect(alertSpy).not.toHaveBeenCalled();

  await user.click(within(dialog).getByRole('button', { name: '知道了' }));

  await waitFor(() => {
    expect(window.location.pathname).toBe('/');
  });
  expect(window.location.search).toBe('');
  await waitFor(() => {
    expect(getPlatformTabPanel('home').getAttribute('aria-hidden')).toBe(
      'false',
    );
  });
  expect(screen.queryByText('详情')).toBeNull();
  expect(screen.queryByText('未找到拼图作品。')).toBeNull();
  expect(startPuzzleRun).toHaveBeenCalledTimes(0);

  alertSpy.mockRestore();
});

test('public code search opens a published big fish work by BF code', async () => {
  const user = userEvent.setup();
  const bigFishWork: BigFishWorkSummary = {
    workId: 'big-fish-work-public-1',
    sourceSessionId: 'big-fish-session-public-1',
    ownerUserId: 'user-2',
    authorDisplayName: '大鱼作者',
    title: '机械深海 大鱼吃小鱼',
    subtitle: '机械微生物吞并进化',
    summary: '从微光孢子一路吞并成长到深海巨鲲。',
    coverImageSrc: null,
    status: 'published',
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    levelCount: 8,
    levelMainImageReadyCount: 8,
    levelMotionReadyCount: 16,
    backgroundReady: true,
  };

  vi.mocked(listBigFishGallery).mockResolvedValue({
    items: [bigFishWork],
  });

  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'BF-NPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  expect(await screen.findByText('详情')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '启动' }));

  await waitFor(() => {
    expect(startBigFishRun).toHaveBeenCalledWith('big-fish-session-public-1');
  });
  expect(await screen.findByText('Lv.1/8 · 进行中')).toBeTruthy();
  expect(getBigFishCreationSession).not.toHaveBeenCalledWith(
    'big-fish-session-public-1',
  );
  expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
});

test('public code search opens a published Match3D work by M3 code and starts runtime', async () => {
  const user = userEvent.setup();
  const match3dWork: Match3DWorkSummary = {
    workId: 'match3d-work-public-1',
    profileId: 'match3d-profile-public-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'match3d-session-public-1',
    gameName: '水果抓大鹅',
    themeText: '水果消除',
    summary: '把圆形空间里的水果全部消除。',
    tags: ['水果', '消除'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 4,
    difficulty: 5,
    publicationStatus: 'published',
    playCount: 3,
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    generatedItemAssets: [],
  };

  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [match3dWork],
  });
  vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
    item: match3dWork,
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dWork.profileId),
  });

  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'M3-EPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  expect(await screen.findByText('详情')).toBeTruthy();
  expect(screen.getByText('水果抓大鹅')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '启动' }));

  await waitFor(() => {
    expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
      'match3d-profile-public-1',
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(
    await screen.findByText(
      '抓大鹅运行态：match3d-run-match3d-profile-public-1',
    ),
  ).toBeTruthy();
  expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
});

test('published Match3D runtime receives persisted generated models', async () => {
  const user = userEvent.setup();
  const match3dWork: Match3DWorkSummary = {
    workId: 'match3d-work-model-1',
    profileId: 'match3d-profile-model-1',
    ownerUserId: 'user-2',
    sourceSessionId: 'match3d-session-model-1',
    gameName: '果园抓大鹅',
    themeText: '果园',
    summary: '消除果园里的水果模型。',
    tags: ['果园', '抓大鹅'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 3,
    difficulty: 5,
    publicationStatus: 'published',
    playCount: 3,
    updatedAt: '2026-04-25T10:30:00.000Z',
    publishedAt: '2026-04-25T10:30:00.000Z',
    publishReady: true,
    generatedItemAssets: [
      {
        itemId: 'match3d-item-1',
        itemName: '草莓',
        imageSrc:
          '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        imageObjectKey:
          'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
        modelSrc:
          '/generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
        modelObjectKey:
          'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
        modelFileName: 'strawberry.glb',
        taskUuid: 'task-strawberry',
        subscriptionKey: 'sub-strawberry',
        status: 'model_ready',
        error: null,
      },
    ],
  };

  vi.mocked(listMatch3DGallery).mockResolvedValue({
    items: [match3dWork],
  });
  match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
    run: buildMockMatch3DRun(match3dWork.profileId),
  });

  render(<TestWrapper withAuth />);
  await openDiscoverHub(user);

  const searchInput =
    await screen.findByPlaceholderText('搜索作品号、名称、作者、描述');
  await user.type(searchInput, 'M3-LEMODEL1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  await user.click(await screen.findByRole('button', { name: '启动' }));

  await waitFor(() => {
    expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
      'match3d-profile-model-1',
      LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
    );
  });
  expect(
    await screen.findByTestId('match3d-runtime-generated-model-count'),
  ).toHaveProperty('textContent', '1');
});

test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '补齐关键锚点',
      summary: '玩家是失职返乡的守灯人。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'clarifying',
      stageLabel: '补齐关键锚点',
      playableNpcCount: 0,
      landmarkCount: 0,
      roleVisualReadyCount: 0,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(getRpgCreationResultView).mockResolvedValue({
    ...buildResultViewForSession(mockSession),
    targetStage: 'agent-workspace',
    resultViewSource: null,
  });

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续创作/u);

  expect(
    await screen.findByText('Agent工作区：custom-world-agent-session-1'),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '开始生成草稿' }));

  await waitFor(() => {
    expect(executeRpgCreationAction).toHaveBeenCalledWith(
      'custom-world-agent-session-1',
      {
        action: 'draft_foundation',
      },
    );
  });

  expect(
    await screen.findByRole('progressbar', {
      name: '世界草稿生成进度',
    }),
  ).toBeTruthy();
  expect(screen.queryByText(/Agent工作区/u)).toBeNull();
  expect(screen.queryByText('当前世界信息')).toBeNull();
  expect(screen.queryByText('回到工作区')).toBeNull();
  expect(screen.queryByText('世界承诺')).toBeNull();
  expect(screen.queryByText(/灯塔与禁航令共同决定谁能穿过死潮/u)).toBeNull();
  expect(screen.queryByText('先告诉我你想做一个怎样的 RPG 世界。')).toBeNull();
});

test('running custom world draft generation can return to creation center with shelf badge', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    buildExistingRpgDraftWork({
      stage: 'clarifying',
      stageLabel: '补齐关键锚点',
      playableNpcCount: 0,
      landmarkCount: 0,
    }),
  ]);
  vi.mocked(getRpgCreationResultView).mockResolvedValue({
    ...buildResultViewForSession(mockSession),
    targetStage: 'agent-workspace',
    resultViewSource: null,
  });

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续创作/u);
  expect(
    await screen.findByText('Agent工作区：custom-world-agent-session-1'),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '开始生成草稿' }));

  expect(
    await screen.findByRole('progressbar', {
      name: '世界草稿生成进度',
    }),
  ).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '返回创作中心' }));

  expect(
    await screen.findByRole('tablist', { name: '创作入口页签' }),
  ).toBeTruthy();
  await openDraftHub(user);

  expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
  await expectDraftHubGeneratingBadgeCountAtLeast(1);
});

test('refresh restores running draft generation progress instead of agent workspace', async () => {
  window.history.replaceState(
    null,
    '',
    '/?customWorldSessionId=custom-world-agent-session-1&customWorldOperationId=operation-draft-foundation-1&customWorldGenerationSource=agent-draft-foundation',
  );
  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'running',
    phaseLabel: '生成世界底稿',
    phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
    progress: 38,
    error: null,
  });

  render(<TestWrapper withAuth />);

  expect(
    await screen.findByRole('progressbar', {
      name: '世界草稿生成进度',
    }),
  ).toBeTruthy();
  expect(screen.queryByText(/Agent工作区/u)).toBeNull();
});

test('failed draft work continues on generation progress view instead of agent workspace', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '失败中的潮雾列岛',
      subtitle: '生成失败待处理',
      summary: '草稿生成过程中失败，需要继续处理。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'clarifying',
      stageLabel: '生成失败待处理',
      playableNpcCount: 0,
      landmarkCount: 0,
      roleVisualReadyCount: 0,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue({
    ...buildResultViewForSession({
      ...mockSession,
      stage: 'error',
    }),
    targetStage: 'custom-world-generating',
    generationViewSource: 'agent-draft-foundation',
    recoveryAction: 'resume_generation',
  });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  expect(await screen.findByText('失败中的潮雾列岛')).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: /继续创作/u }));

  expect(
    await screen.findByRole('progressbar', {
      name: '世界草稿生成进度',
    }),
  ).toBeTruthy();
  expect(screen.queryByText(/Agent工作区/u)).toBeNull();
});

test('existing draft sessions open result page refinement instead of agent dialog', async () => {
  const user = userEvent.setup();

  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'completed',
    phaseLabel: '世界底稿已生成',
    phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
    progress: 100,
    error: null,
  });
  vi.mocked(getRpgCreationSession).mockResolvedValue(compiledAgentDraftSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(compiledAgentDraftSession),
  );
  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '待完善草稿',
      summary: '玩家是失职返乡的守灯人。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'object_refining',
      stageLabel: '待完善草稿',
      playableNpcCount: 3,
      landmarkCount: 4,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: '沈砺 · 主图已生成',
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续完善/u);

  await waitFor(
    async () => {
      expect(await screen.findByText('世界档案')).toBeTruthy();
      expect(screen.getByText('已自动保存')).toBeTruthy();
      expect(screen.getByRole('button', { name: '作品测试' })).toBeTruthy();
      expect(screen.getByRole('button', { name: '发布' })).toBeTruthy();
    },
    { timeout: 2500 },
  );

  expect(screen.queryByText(/Agent工作区/u)).toBeNull();
  expect(screen.queryByRole('button', { name: /^锚点/u })).toBeNull();
  expect(screen.getByText(/基本设定/u)).toBeTruthy();
  expect(screen.queryByRole('button', { name: /新增场景角色/u })).toBeNull();

  await user.click(screen.getByRole('tab', { name: /场景角色/u }));
  expect(screen.getByRole('button', { name: /顾潮音/u })).toBeTruthy();
  await user.click(screen.getByRole('button', { name: /顾潮音/u }));
  expect(await screen.findByText(/编辑场景角色：顾潮音/u)).toBeTruthy();
  expect(screen.getByRole('button', { name: /AI生成/u })).toBeTruthy();
});

test('agent result view shows publish blocker dialog before publish action when preview gate is not ready', async () => {
  const user = userEvent.setup();

  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'completed',
    phaseLabel: '世界底稿已生成',
    phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
    progress: 100,
    error: null,
  });
  const blockedSession = {
    ...compiledAgentDraftSession,
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: false,
      blockers: [
        {
          id: 'publish-role-assets-incomplete',
          code: 'publish_role_assets_incomplete',
          message: '仍有角色缺少正式主图或动作资产，发布前需要先补齐。',
        },
      ],
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getRpgCreationSession).mockResolvedValue(blockedSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(blockedSession),
  );
  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '待发布草稿',
      summary: '当前草稿已经补齐八锚点与第一幕。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'ready_to_publish',
      stageLabel: '待发布草稿',
      playableNpcCount: 3,
      landmarkCount: 1,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续完善/u);

  const actionButton = await screen.findByRole(
    'button',
    { name: '发布' },
    { timeout: 5000 },
  );
  expect((actionButton as HTMLButtonElement).disabled).toBe(false);

  const publishWorldCallCountBeforeClick = vi
    .mocked(executeRpgCreationAction)
    .mock.calls.filter(
      ([sessionId, payload]) =>
        sessionId === 'custom-world-agent-session-1' &&
        payload?.action === 'publish_world',
    ).length;

  await user.click(actionButton);

  expect(await screen.findByRole('dialog', { name: '发布作品' })).toBeTruthy();
  expect(screen.getByText('发布检查')).toBeTruthy();
  expect(screen.getByText('封面设置')).toBeTruthy();
  expect(screen.getByText(/仍有角色缺少正式主图或动作资产/u)).toBeTruthy();

  const publishWorldCallCountAfterClick = vi
    .mocked(executeRpgCreationAction)
    .mock.calls.filter(
      ([sessionId, payload]) =>
        sessionId === 'custom-world-agent-session-1' &&
        payload?.action === 'publish_world',
    ).length;
  expect(publishWorldCallCountAfterClick).toBe(
    publishWorldCallCountBeforeClick,
  );
});

test('agent draft result publishes to gallery from publish panel', async () => {
  const user = userEvent.setup();
  const handleCustomWorldSelect = vi.fn();

  const publishReadyDraftSession = {
    ...compiledAgentDraftSession,
    stage: 'ready_to_publish' as const,
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: true,
      canEnterWorld: false,
      blockers: [],
    },
  } satisfies CustomWorldAgentSessionSnapshot;

  const publishedSession = {
    ...publishReadyDraftSession,
    stage: 'published' as const,
    resultPreview: {
      ...publishReadyDraftSession.resultPreview!,
      publishReady: true,
      canEnterWorld: true,
      blockers: [],
      preview: {
        ...publishReadyDraftSession.resultPreview!.preview,
        id: 'agent-draft-custom-world-agent-session-1',
        name: '潮雾列岛·已发布',
        summary: '发布完成后应直接使用已发布预览进入世界。',
      },
    },
  } satisfies CustomWorldAgentSessionSnapshot;

  let hasPublishedWorld = false;
  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '待发布草稿',
      summary: '当前草稿已经准备发布。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'ready_to_publish',
      stageLabel: '待发布草稿',
      playableNpcCount: 3,
      landmarkCount: 1,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-publish-world-1',
    type: 'publish_world',
    status: 'completed',
    phaseLabel: '世界已发布',
    phaseDetail: '正式世界档案已写入作品库。',
    progress: 100,
    error: null,
  });
  vi.mocked(executeRpgCreationAction).mockImplementation(async (_, payload) => {
    if (payload.action === 'publish_world') {
      hasPublishedWorld = true;
    }

    return {
      operation: {
        operationId: 'operation-publish-world-1',
        type: 'publish_world',
        status: 'queued',
        phaseLabel: '执行发布校验',
        phaseDetail: '正在检查角色资产、场景图和主线草稿是否满足发布门槛。',
        progress: 28,
        error: null,
      },
    };
  });
  vi.mocked(getRpgCreationSession).mockImplementation(async () =>
    hasPublishedWorld ? publishedSession : publishReadyDraftSession,
  );
  vi.mocked(getRpgCreationResultView).mockImplementation(async () =>
    buildResultViewForSession(
      hasPublishedWorld ? publishedSession : publishReadyDraftSession,
    ),
  );

  function PublishFlowWrapper() {
    const [selectionStage, setSelectionStage] =
      useState<SelectionStage>('platform');

    return (
      <AuthUiContext.Provider value={createAuthValue()}>
        <RpgEntryFlowShell
          selectionStage={selectionStage}
          setSelectionStage={setSelectionStage}
          hasSavedGame={false}
          savedSnapshot={null}
          handleContinueGame={() => {}}
          handleStartNewGame={() => {}}
          handleCustomWorldSelect={handleCustomWorldSelect}
        />
      </AuthUiContext.Provider>
    );
  }

  render(<PublishFlowWrapper />);

  await openExistingRpgDraft(user, /继续完善/u);

  const actionButton = await screen.findByRole(
    'button',
    {
      name: '发布',
    },
    { timeout: 5000 },
  );
  await user.click(actionButton);
  await user.click(await screen.findByRole('button', { name: '发布到广场' }));

  await waitFor(() => {
    expect(
      vi
        .mocked(executeRpgCreationAction)
        .mock.calls.some(
          ([sessionId, payload]) =>
            sessionId === 'custom-world-agent-session-1' &&
            payload?.action === 'publish_world',
        ),
    ).toBe(true);
  });
  expect(handleCustomWorldSelect).not.toHaveBeenCalled();
});

test('agent draft result test button enters current draft without publish gate', async () => {
  const user = userEvent.setup();
  const handleCustomWorldSelect = vi.fn();

  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'completed',
    phaseLabel: '世界底稿已生成',
    phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
    progress: 100,
    error: null,
  });
  const testDraftSession = {
    ...compiledAgentDraftSession,
    stage: 'ready_to_publish',
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: false,
      canEnterWorld: true,
      blockers: [
        {
          id: 'missing-cover-image',
          code: 'MISSING_COVER_IMAGE',
          message: '发布前需要补齐作品封面。',
        },
      ],
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getRpgCreationSession).mockResolvedValue(testDraftSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(testDraftSession),
  );
  mockExistingRpgDraftShelf({
    subtitle: '待发布草稿',
    summary: '当前草稿已经准备测试。',
    stage: 'ready_to_publish',
    stageLabel: '待发布草稿',
    landmarkCount: 1,
    roleAssetSummaryLabel: null,
  });

  function TestDraftWrapper() {
    const [selectionStage, setSelectionStage] =
      useState<SelectionStage>('platform');

    return (
      <AuthUiContext.Provider value={createAuthValue()}>
        <RpgEntryFlowShell
          selectionStage={selectionStage}
          setSelectionStage={setSelectionStage}
          hasSavedGame={false}
          savedSnapshot={null}
          handleContinueGame={() => {}}
          handleStartNewGame={() => {}}
          handleCustomWorldSelect={handleCustomWorldSelect}
        />
      </AuthUiContext.Provider>
    );
  }

  render(<TestDraftWrapper />);

  await openExistingRpgDraft(user, /继续完善/u);
  await screen.findByText('世界档案', {}, { timeout: 5000 });
  await user.click(
    await screen.findByRole('button', { name: '作品测试' }, { timeout: 5000 }),
  );

  await waitFor(() => {
    expect(handleCustomWorldSelect).toHaveBeenCalledWith(
      expect.objectContaining({ name: '潮雾列岛' }),
      expect.objectContaining({
        mode: 'play',
        disablePersistence: true,
        returnStage: 'custom-world-result',
      }),
    );
  });
  expect(
    vi
      .mocked(executeRpgCreationAction)
      .mock.calls.some(
        ([sessionId, payload]) =>
          sessionId === 'custom-world-agent-session-1' &&
          payload?.action === 'publish_world',
      ),
  ).toBe(false);
}, 10_000);

test('agent draft result test button enters the opened draft profile instead of a previous session', async () => {
  const user = userEvent.setup();
  const handleCustomWorldSelect = vi.fn();

  const previousDraftSession = {
    ...compiledAgentDraftSession,
    sessionId: 'custom-world-agent-session-1',
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: false,
      canEnterWorld: true,
      preview: {
        ...compiledAgentResultPreview,
        id: 'agent-draft-custom-world-agent-session-1',
        name: '潮雾列岛',
        summary: '上一份草稿内容，不能被本次启动复用。',
        playableNpcs: [
          {
            ...compiledAgentResultPreview.playableNpcs[0]!,
            id: 'playable-previous-1',
            name: '沈砺',
          },
        ],
        sessionId: 'custom-world-agent-session-1',
      },
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  const openedDraftSession = {
    ...compiledAgentDraftSession,
    sessionId: 'custom-world-agent-session-2',
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: false,
      canEnterWorld: true,
      preview: {
        ...compiledAgentResultPreview,
        id: 'agent-draft-custom-world-agent-session-2',
        name: '星砂废都',
        subtitle: '坠星沙海与废都钟楼',
        summary: '本次从草稿架打开的目标草稿内容。',
        playerGoal: '找到废都钟楼下被星砂掩埋的旧约。',
        playableNpcs: [
          {
            ...compiledAgentResultPreview.playableNpcs[0]!,
            id: 'playable-opened-1',
            name: '砂眠',
            title: '废都引路人',
          },
        ],
        storyNpcs: [],
        landmarks: [
          {
            ...compiledAgentResultPreview.landmarks[0]!,
            id: 'landmark-opened-1',
            name: '坠星钟楼',
          },
        ],
        sessionId: 'custom-world-agent-session-2',
      },
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  const sessionsById = new Map([
    [previousDraftSession.sessionId, previousDraftSession],
    [openedDraftSession.sessionId, openedDraftSession],
  ]);

  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'completed',
    phaseLabel: '世界底稿已生成',
    phaseDetail: '第一版世界底稿和草稿卡已经整理完成。',
    progress: 100,
    error: null,
  });
  vi.mocked(getRpgCreationSession).mockImplementation(async (sessionId) => {
    const session = sessionsById.get(sessionId);
    if (!session) {
      throw new Error(`Missing test session: ${sessionId}`);
    }
    return session;
  });
  vi.mocked(getRpgCreationResultView).mockImplementation(async (sessionId) => {
    const session = sessionsById.get(sessionId);
    if (!session) {
      throw new Error(`Missing test result view: ${sessionId}`);
    }
    return buildResultViewForSession(session);
  });
  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    buildExistingRpgDraftWork({
      workId: 'draft:custom-world-agent-session-1',
      title: '潮雾列岛',
      summary: '上一份草稿内容，不能被本次启动复用。',
      sessionId: 'custom-world-agent-session-1',
      playableNpcCount: 1,
      landmarkCount: 1,
    }),
    buildExistingRpgDraftWork({
      workId: 'draft:custom-world-agent-session-2',
      title: '星砂废都',
      subtitle: '待完善草稿',
      summary: '本次从草稿架打开的目标草稿内容。',
      sessionId: 'custom-world-agent-session-2',
      playableNpcCount: 1,
      landmarkCount: 1,
    }),
  ]);

  render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);

  await openDraftHub(user);
  const draftPanel = getPlatformTabPanel('saves');
  await user.click(
    await within(draftPanel).findByRole('button', {
      name: /继续完善《星砂废都》/u,
    }),
  );
  expect(
    await screen.findByText('世界档案', {}, { timeout: 5000 }),
  ).toBeTruthy();
  expect(screen.getByText('星砂废都')).toBeTruthy();

  await user.click(
    await screen.findByRole('button', { name: '作品测试' }, { timeout: 5000 }),
  );

  await waitFor(() => {
    expect(handleCustomWorldSelect).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 'agent-draft-custom-world-agent-session-2',
        name: '星砂废都',
        summary: '本次从草稿架打开的目标草稿内容。',
        playableNpcs: [
          expect.objectContaining({
            id: 'playable-opened-1',
            name: '砂眠',
          }),
        ],
      }),
      expect.objectContaining({
        mode: 'play',
        disablePersistence: true,
        returnStage: 'custom-world-result',
      }),
    );
  });
  expect(
    vi
      .mocked(executeRpgCreationAction)
      .mock.calls.some(([, payload]) => payload?.action === 'publish_world'),
  ).toBe(false);
}, 10_000);

test('agent draft result start button enters the opened published draft profile instead of a previous session', async () => {
  const user = userEvent.setup();
  const handleCustomWorldSelect = vi.fn();

  const previousDraftSession = {
    ...compiledAgentDraftSession,
    sessionId: 'custom-world-agent-session-1',
    stage: 'published',
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: true,
      canEnterWorld: true,
      preview: {
        ...compiledAgentResultPreview,
        id: 'agent-draft-custom-world-agent-session-1',
        name: '潮雾列岛',
        summary: '上一份已发布草稿内容，不能被本次启动复用。',
        sessionId: 'custom-world-agent-session-1',
      },
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  const openedPublishedDraftSession = {
    ...compiledAgentDraftSession,
    sessionId: 'custom-world-agent-session-2',
    stage: 'published',
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: true,
      canEnterWorld: true,
      preview: {
        ...compiledAgentResultPreview,
        id: 'agent-draft-custom-world-agent-session-2',
        name: '星砂废都',
        subtitle: '坠星沙海与废都钟楼',
        summary: '本次从草稿架打开且已发布的目标草稿内容。',
        playableNpcs: [
          {
            ...compiledAgentResultPreview.playableNpcs[0]!,
            id: 'playable-opened-1',
            name: '砂眠',
            title: '废都引路人',
          },
        ],
        sessionId: 'custom-world-agent-session-2',
      },
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  const sessionsById = new Map([
    [previousDraftSession.sessionId, previousDraftSession],
    [openedPublishedDraftSession.sessionId, openedPublishedDraftSession],
  ]);

  vi.mocked(getRpgCreationSession).mockImplementation(async (sessionId) => {
    const session = sessionsById.get(sessionId);
    if (!session) {
      throw new Error(`Missing test session: ${sessionId}`);
    }
    return session;
  });
  vi.mocked(getRpgCreationResultView).mockImplementation(async (sessionId) => {
    const session = sessionsById.get(sessionId);
    if (!session) {
      throw new Error(`Missing test result view: ${sessionId}`);
    }
    return buildResultViewForSession(session);
  });
  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    buildExistingRpgDraftWork({
      workId: 'draft:custom-world-agent-session-1',
      title: '潮雾列岛',
      summary: '上一份已发布草稿内容，不能被本次启动复用。',
      stage: 'published',
      stageLabel: '已发布',
      sessionId: 'custom-world-agent-session-1',
      playableNpcCount: 1,
      landmarkCount: 1,
      canEnterWorld: true,
    }),
    buildExistingRpgDraftWork({
      workId: 'draft:custom-world-agent-session-2',
      title: '星砂废都',
      subtitle: '已发布草稿',
      summary: '本次从草稿架打开且已发布的目标草稿内容。',
      stage: 'published',
      stageLabel: '已发布',
      sessionId: 'custom-world-agent-session-2',
      playableNpcCount: 1,
      landmarkCount: 1,
      canEnterWorld: true,
    }),
  ]);

  render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);

  await openDraftHub(user);
  const draftPanel = getPlatformTabPanel('saves');
  await user.click(
    await within(draftPanel).findByRole('button', {
      name: /继续完善《星砂废都》/u,
    }),
  );
  expect(
    await screen.findByText('世界档案', {}, { timeout: 5000 }),
  ).toBeTruthy();
  expect(screen.getByText('星砂废都')).toBeTruthy();

  await user.click(
    await screen.findByRole('button', { name: '进入世界' }, { timeout: 5000 }),
  );

  await waitFor(() => {
    expect(handleCustomWorldSelect).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 'agent-draft-custom-world-agent-session-2',
        name: '星砂废都',
        summary: '本次从草稿架打开且已发布的目标草稿内容。',
        playableNpcs: [
          expect.objectContaining({
            id: 'playable-opened-1',
            name: '砂眠',
          }),
        ],
      }),
    );
  });
  expect(
    vi
      .mocked(executeRpgCreationAction)
      .mock.calls.some(([, payload]) => payload?.action === 'publish_world'),
  ).toBe(false);
}, 10_000);

test('agent result view does not keep legacy publish blockers when preview uses anchorContent and sceneChapterBlueprints', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'draft:custom-world-agent-session-1',
      sourceType: 'agent_session',
      status: 'draft',
      title: '潮雾列岛',
      subtitle: '待发布草稿',
      summary: '当前草稿已经补齐八锚点与第一幕。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: null,
      stage: 'ready_to_publish',
      stageLabel: '待发布草稿',
      playableNpcCount: 3,
      landmarkCount: 1,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: 'custom-world-agent-session-1',
      profileId: null,
      canResume: true,
      canEnterWorld: false,
    },
  ]);
  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'completed',
    phaseLabel: '世界底稿已生成',
    phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
    progress: 100,
    error: null,
  });
  const publishGateSession = {
    ...compiledAgentDraftSession,
    stage: 'ready_to_publish',
    resultPreview: {
      ...compiledAgentDraftSession.resultPreview!,
      publishReady: true,
      blockers: [],
      preview: {
        ...compiledAgentResultPreview,
        settingText: '被海雾吞没的旧航路群岛',
        anchorContent: {
          worldPromise:
            '被海雾吞没的旧航路群岛，灯塔与禁航令共同决定谁能穿过死潮，体验压抑、潮湿、悬疑。',
          playerFantasy:
            '玩家是被迫返乡的守灯人继承者，追查沉船夜与假航灯的关系，风险是失去家族最后一条可信航线。',
          themeBoundary: '压抑、悬疑；潮湿群岛、冷雾港口；避免轻喜冒险。',
          playerEntryPoint:
            '玩家以返乡守灯人继承者身份切入，回港首夜撞见禁航区假航灯重亮，动机是阻止更多船只误入死潮。',
          coreConflict:
            '守灯会与航运公会争夺航路解释权，有人在借假航灯持续清洗旧案证据，玩家返乡当夜就被卷进封航冲突。',
          keyRelationships: null,
          hiddenLines:
            '沉船夜与假航灯骗局属于同一操盘链条，表面像海雾自然失控，揭示节奏是先见异常，再见旧案，再见操盘者。',
          iconicElements:
            '假航灯、沉钟回响、旧灯塔、禁航碑；错误航灯会把船引进必死水域。',
        },
        creatorIntent: {
          sourceMode: 'card',
          rawSettingText: '',
          worldHook: '被海雾吞没的旧航路群岛',
          themeKeywords: ['海雾', '旧航路'],
          toneDirectives: ['压抑', '悬疑'],
          playerPremise: '玩家回到群岛调查沉船真相。',
          openingSituation: '首夜就有陌生船只闯入禁航区。',
          coreConflicts: ['航运公会与守灯会争夺航路控制权'],
          keyFactions: [],
          keyCharacters: [],
          keyLandmarks: [],
          iconicElements: ['会移动的海雾'],
          forbiddenDirectives: [],
        },
        sceneChapterBlueprints: [
          {
            id: 'scene-chapter-1',
            sceneId: 'landmark-1',
            title: '沉钟栈桥章节',
            summary: '围绕沉钟栈桥推进的三幕结构。',
            linkedThreadIds: [],
            linkedLandmarkIds: ['landmark-1'],
            acts: [
              {
                id: 'scene-act-1',
                sceneId: 'landmark-1',
                title: '潮声逼近',
                summary: '第一幕先把潮声与旧钟压上来。',
                stageCoverage: ['opening'],
                encounterNpcIds: ['story-1'],
                primaryNpcId: 'story-1',
                linkedThreadIds: [],
                advanceRule: 'after_primary_contact',
                actGoal: '接住首幕压力',
                transitionHook: '继续逼近钟楼深处。',
              },
            ],
          },
        ],
      },
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getRpgCreationSession).mockResolvedValue(publishGateSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(publishGateSession),
  );

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  expect(await screen.findByRole('button', { name: /继续完善/u })).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: /继续完善/u }));

  await waitFor(() => {
    expect(screen.getByRole('button', { name: '作品测试' })).toBeTruthy();
    expect(screen.getByRole('button', { name: '发布' })).toBeTruthy();
  });

  expect(screen.queryByText(/当前还有 4 个发布阻断项/u)).toBeNull();
  const actionButton = screen.getByRole('button', {
    name: '发布',
  });
  expect((actionButton as HTMLButtonElement).disabled).toBe(false);
});

test('agent draft result back button returns to draft hub without syncing result profile', async () => {
  const user = userEvent.setup();

  const resultSession = {
    ...mockSession,
    stage: 'object_refining' as const,
    creatorIntent: {
      sourceMode: 'card',
      worldHook: '被海雾吞没的旧航路群岛',
      playerPremise: '玩家回到群岛调查沉船真相。',
      themeKeywords: ['海雾', '旧航路'],
      toneDirectives: ['压抑', '悬疑'],
      openingSituation: '首夜就有陌生船只闯入禁航区。',
      coreConflicts: ['航运公会与守灯会争夺航路控制权'],
      keyFactions: [],
      keyCharacters: [],
      keyLandmarks: [],
      iconicElements: ['会移动的海雾'],
      forbiddenDirectives: [],
      rawSettingText: '',
    },
    draftProfile: {
      name: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '第一版世界底稿已经整理完成。',
      tone: '压抑、潮湿、悬疑',
      playerGoal: '查清沉船与禁航区异动的真相。',
      majorFactions: ['守灯会', '航运公会'],
      coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
      playableNpcs: [
        {
          id: 'playable-1',
          name: '沈砺',
          title: '旧航路引路人',
          role: '关键同行者',
          publicIdentity: '最熟悉旧航路的人。',
          publicMask: '看上去像可靠旧友。',
          currentPressure: '他必须在两股势力间站队。',
          hiddenHook: '暗中替沉船商盟引路。',
          relationToPlayer: '旧友兼潜在背叛者',
          threadIds: ['thread-1'],
          summary: '他像旧友，但也像一把始终没收回鞘的刀。',
        },
      ],
      storyNpcs: [
        {
          id: 'story-1',
          name: '顾潮音',
          title: '守灯会值夜人',
          role: '场景关键角色',
          publicIdentity: '负责夜间巡灯与封锁。',
          publicMask: '对外一直冷静克制。',
          currentPressure: '她知道更多禁航区真相。',
          hiddenHook: '曾亲眼见过失控海雾吞船。',
          relationToPlayer: '最早愿意交换线索的人',
          threadIds: ['thread-1'],
          summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
        },
      ],
      landmarks: [
        {
          id: 'landmark-1',
          name: '回潮旧灯塔',
          purpose: '观察雾潮与往来船只',
          mood: '潮湿、压抑、风声不止',
          importance: '开局核心场景',
          characterIds: ['story-1'],
          threadIds: ['thread-1'],
          summary: '旧灯塔是整片群岛最先看见异动的地方。',
        },
      ],
      factions: [],
      threads: [],
      chapters: [],
      worldHook: '被海雾吞没的旧航路群岛',
      playerPremise: '玩家回到群岛调查沉船真相。',
      openingSituation: '首夜就有陌生船只闯入禁航区。',
      iconicElements: ['会移动的海雾'],
      sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
      legacyResultProfile: {
        id: 'agent-draft-custom-world-agent-session-1',
        settingText: '被海雾吞没的旧航路群岛',
        name: '潮雾列岛·同步后',
        subtitle: '旧灯塔与失控航路',
        summary: '同步后的结果页快照已经回写到 session。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清沉船与禁航区异动的真相。',
        templateWorldType: 'WUXIA',
        majorFactions: ['守灯会', '航运公会'],
        coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
        playableNpcs: [],
        storyNpcs: [],
        items: [],
        landmarks: [],
        generationMode: 'full',
        generationStatus: 'complete',
      },
    },
    draftCards: [
      {
        id: 'world-foundation',
        kind: 'world',
        title: '潮雾列岛',
        subtitle: '旧灯塔与失控航路',
        summary: '第一版世界底稿已经整理完成。',
        status: 'warning',
        linkedIds: ['playable-1', 'story-1', 'landmark-1'],
        warningCount: 0,
      },
    ],
    resultPreview: {
      source: 'session_preview' as const,
      preview: {
        id: 'agent-draft-custom-world-agent-session-1',
        settingText: '被海雾吞没的旧航路群岛',
        name: '潮雾列岛·同步后',
        subtitle: '旧灯塔与失控航路',
        summary: '同步后的结果页快照已经回写到 session。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清沉船与禁航区异动的真相。',
        templateWorldType: 'WUXIA',
        majorFactions: ['守灯会', '航运公会'],
        coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
        playableNpcs: [],
        storyNpcs: [],
        items: [],
        landmarks: [],
        generationMode: 'full',
        generationStatus: 'complete',
        sessionId: 'custom-world-agent-session-1',
      },
      generatedAt: '2026-04-20T12:00:00.000Z',
      qualityFindings: [],
      blockers: [],
      publishReady: false,
      canEnterWorld: false,
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getRpgCreationSession).mockResolvedValue(resultSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(resultSession),
  );
  mockExistingRpgDraftShelf({
    summary: '同步后的结果页快照已经回写到 session。',
  });

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续完善/u);

  await waitFor(
    async () => {
      expect(await screen.findByText('世界档案')).toBeTruthy();
      expect(screen.getByRole('button', { name: /返回创作/u })).toBeTruthy();
    },
    { timeout: 2500 },
  );
  const syncCallsBeforeBack = vi
    .mocked(executeRpgCreationAction)
    .mock.calls.filter(
      ([sessionId, payload]) =>
        sessionId === 'custom-world-agent-session-1' &&
        payload?.action === 'sync_result_profile',
    ).length;

  await user.click(screen.getByRole('button', { name: /返回创作/u }));

  const draftPanel = await findPlatformTabPanel('saves');
  await waitFor(() => {
    expect(draftPanel.getAttribute('aria-hidden')).toBe('false');
  });
  expect(
    within(draftPanel).getByRole('tablist', { name: '作品筛选' }),
  ).toBeTruthy();
  expect(getPlatformTabPanel('create').getAttribute('aria-hidden')).toBe(
    'true',
  );

  expect(
    vi
      .mocked(executeRpgCreationAction)
      .mock.calls.filter(
        ([sessionId, payload]) =>
          sessionId === 'custom-world-agent-session-1' &&
          payload?.action === 'sync_result_profile',
      ).length,
  ).toBe(syncCallsBeforeBack);
  expect(screen.queryByText('世界档案')).toBeNull();
});

test('agent draft result auto-save syncs result profile before persisting backend result view', async () => {
  const user = userEvent.setup();

  const syncedSession = {
    ...mockSession,
    stage: 'object_refining' as const,
    creatorIntent: {
      sourceMode: 'card',
      worldHook: '被海雾吞没的旧航路群岛',
      playerPremise: '玩家回到群岛调查沉船真相。',
      themeKeywords: ['海雾', '旧航路'],
      toneDirectives: ['压抑', '悬疑'],
      openingSituation: '首夜就有陌生船只闯入禁航区。',
      coreConflicts: ['航运公会与守灯会争夺航路控制权'],
      keyFactions: [],
      keyCharacters: [],
      keyLandmarks: [],
      iconicElements: ['会移动的海雾'],
      forbiddenDirectives: [],
      rawSettingText: '',
    },
    draftProfile: {
      name: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '第一版世界底稿已经整理完成。',
      tone: '压抑、潮湿、悬疑',
      playerGoal: '查清沉船与禁航区异动的真相。',
      majorFactions: ['守灯会', '航运公会'],
      coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
      playableNpcs: [],
      storyNpcs: [],
      landmarks: [],
      factions: [],
      threads: [],
      chapters: [],
      worldHook: '被海雾吞没的旧航路群岛',
      playerPremise: '玩家回到群岛调查沉船真相。',
      openingSituation: '首夜就有陌生船只闯入禁航区。',
      iconicElements: ['会移动的海雾'],
      sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
      legacyResultProfile: {
        id: 'agent-draft-custom-world-agent-session-1',
        settingText: '被海雾吞没的旧航路群岛',
        name: '潮雾列岛·session最新版',
        subtitle: '旧灯塔与失控航路',
        summary: '作品库应该保存这份同步后的最新快照。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清沉船与禁航区异动的真相。',
        templateWorldType: 'WUXIA',
        majorFactions: ['守灯会', '航运公会'],
        coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
        playableNpcs: [],
        storyNpcs: [],
        items: [],
        landmarks: [],
        generationMode: 'full',
        generationStatus: 'complete',
      },
    },
    draftCards: [
      {
        id: 'world-foundation',
        kind: 'world',
        title: '潮雾列岛',
        subtitle: '旧灯塔与失控航路',
        summary: '第一版世界底稿已经整理完成。',
        status: 'warning',
        linkedIds: [],
        warningCount: 0,
      },
    ],
    resultPreview: {
      source: 'session_preview' as const,
      preview: {
        id: 'agent-draft-custom-world-agent-session-1',
        settingText: '被海雾吞没的旧航路群岛',
        name: '潮雾列岛·session最新版',
        subtitle: '旧灯塔与失控航路',
        summary: '作品库应该保存这份同步后的最新快照。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清沉船与禁航区异动的真相。',
        templateWorldType: 'WUXIA',
        majorFactions: ['守灯会', '航运公会'],
        coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
        playableNpcs: [],
        storyNpcs: [],
        items: [],
        landmarks: [],
        generationMode: 'full',
        generationStatus: 'complete',
        sessionId: 'custom-world-agent-session-1',
      },
      generatedAt: '2026-04-20T12:00:00.000Z',
      qualityFindings: [],
      blockers: [],
      publishReady: false,
      canEnterWorld: false,
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getRpgCreationSession).mockResolvedValue(syncedSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(syncedSession),
  );
  mockExistingRpgDraftShelf({
    summary: '作品库应该保存这份同步后的最新快照。',
  });
  vi.mocked(executeRpgCreationAction).mockImplementation(
    async (_, payload) => ({
      operation: {
        operationId:
          payload.action === 'sync_result_profile'
            ? 'operation-sync-result-profile-1'
            : 'operation-draft-foundation-1',
        type: payload.action,
        status: 'queued',
        phaseLabel: '已接收请求',
        phaseDetail:
          payload.action === 'sync_result_profile'
            ? '正在同步结果页档案。'
            : '正在准备生成世界底稿。',
        progress: 10,
        error: null,
      },
    }),
  );
  vi.mocked(getRpgCreationOperation).mockResolvedValue({
    operationId: 'operation-sync-result-profile-1',
    type: 'sync_result_profile',
    status: 'completed',
    phaseLabel: '结果页档案已同步',
    phaseDetail: '服务端已根据最新结果页档案刷新会话预览。',
    progress: 100,
    error: null,
  });

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续完善/u);

  await waitFor(
    async () => {
      expect(await screen.findByText('世界档案')).toBeTruthy();
      expect(screen.getByText('已自动保存')).toBeTruthy();
    },
    { timeout: 2500 },
  );

  await waitFor(() => {
    expect(upsertRpgWorldProfile).toHaveBeenCalled();
  });

  const latestSavedProfile = vi
    .mocked(upsertRpgWorldProfile)
    .mock.calls.at(-1)?.[0];
  const latestSaveRequest = vi
    .mocked(upsertRpgWorldProfile)
    .mock.calls.at(-1)?.[1];
  expect(latestSavedProfile?.name).toBe('潮雾列岛·session最新版');
  expect(latestSavedProfile?.summary).toBe(
    '作品库应该保存这份同步后的最新快照。',
  );
  expect(latestSaveRequest).toEqual({
    sourceAgentSessionId: 'custom-world-agent-session-1',
  });
  expect(
    vi
      .mocked(executeRpgCreationAction)
      .mock.calls.some(
        ([sessionId, payload]) =>
          sessionId === 'custom-world-agent-session-1' &&
          payload?.action === 'sync_result_profile',
      ),
  ).toBe(true);
});

test('agent draft result can open from server result preview without embedded legacyResultProfile', async () => {
  const user = userEvent.setup();

  const previewOnlySession = {
    ...compiledAgentDraftSession,
    draftProfile: {
      ...compiledAgentDraftSession.draftProfile,
      playableNpcs: [],
      storyNpcs: [],
      landmarks: [],
    },
    resultPreview: {
      source: 'session_preview' as const,
      preview: {
        id: 'agent-draft-custom-world-agent-session-1',
        settingText: '被海雾吞没的旧航路群岛',
        name: '潮雾列岛·服务端预览',
        subtitle: '结果页改为优先消费 session.resultPreview',
        summary:
          '即使 draft 中没有 legacyResultProfile，也应该正常打开结果页。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清沉船与禁航区异动的真相。',
        templateWorldType: 'WUXIA',
        majorFactions: ['守灯会', '航运公会'],
        coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
        playableNpcs: [],
        storyNpcs: [],
        items: [],
        landmarks: [],
        generationMode: 'full',
        generationStatus: 'complete',
        sessionId: 'custom-world-agent-session-1',
      },
      generatedAt: '2026-04-20T12:00:00.000Z',
      qualityFindings: [],
      blockers: [],
    },
  } satisfies CustomWorldAgentSessionSnapshot;

  vi.mocked(getRpgCreationSession).mockResolvedValue(previewOnlySession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(previewOnlySession),
  );
  mockExistingRpgDraftShelf({
    summary: '即使 draft 中没有 legacyResultProfile，也应该正常打开结果页。',
  });

  render(<TestWrapper withAuth />);

  await openExistingRpgDraft(user, /继续完善/u);

  await waitFor(
    async () => {
      expect(await screen.findByText('世界档案')).toBeTruthy();
      expect(screen.getByText('潮雾列岛·服务端预览')).toBeTruthy();
      expect(
        screen.getAllByText('结果页改为优先消费 session.resultPreview').length,
      ).toBeGreaterThan(0);
    },
    { timeout: 2500 },
  );
});

test('authenticated users can open save archives from the profile played panel', async () => {
  const user = userEvent.setup();

  vi.mocked(listProfileSaveArchives).mockResolvedValue([
    {
      worldKey: 'custom:world-1',
      ownerUserId: null,
      profileId: 'world-1',
      worldType: 'CUSTOM',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '回到旧灯塔继续推进调查。',
      coverImageSrc: null,
      lastPlayedAt: '2026-04-19T12:00:00.000Z',
    },
  ]);

  render(<TestWrapper withAuth />);

  await openProfilePlayedWorks(user);

  expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
  expect(screen.getAllByText('旧灯塔与失控航路').length).toBeGreaterThan(0);
  expect(screen.queryByText('SAVE ARCHIVE')).toBeNull();
  expect(screen.queryByText('ARCHIVE')).toBeNull();
  expect(screen.queryByText('最近存档')).toBeNull();
});

test('puzzle save archive highlights work title and level subtitle', async () => {
  const user = userEvent.setup();

  vi.mocked(listProfileSaveArchives).mockResolvedValue([
    {
      worldKey: 'puzzle:puzzle-profile-1',
      ownerUserId: 'user-2',
      profileId: 'puzzle-profile-1',
      worldType: 'PUZZLE',
      worldName: '雨夜猫塔',
      subtitle: '第 2 关 · 星桥机关',
      summaryText: '拼图进行中',
      coverImageSrc: '/generated-puzzle-assets/puzzle-1/level-2.png',
      lastPlayedAt: '2026-04-19T12:00:00.000Z',
    },
  ]);

  render(<TestWrapper withAuth />);

  await openProfilePlayedWorks(user);

  expect((await screen.findAllByText('雨夜猫塔')).length).toBeGreaterThan(0);
  expect(screen.getAllByText('第 2 关 · 星桥机关').length).toBeGreaterThan(0);
  expect(screen.queryByText('ARCHIVE')).toBeNull();
  expect(screen.queryByText('最近存档')).toBeNull();
});

test('manual tab switch is preserved after platform bootstrap requests finish', async () => {
  const user = userEvent.setup();

  let resolveGalleryRequest!: (value: []) => void;
  const delayedGalleryRequest = new Promise<[]>((resolve) => {
    resolveGalleryRequest = resolve;
  });

  vi.mocked(listRpgEntryWorldGallery).mockReturnValueOnce(
    delayedGalleryRequest as Promise<[]>,
  );

  render(<TestWrapper withAuth />);

  await clickFirstButtonByName(user, '创作');
  expect(
    await screen.findByRole('tablist', { name: '创作入口页签' }),
  ).toBeTruthy();

  resolveGalleryRequest([]);

  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('create')).getByRole('tablist', {
        name: '创作入口页签',
      }),
    ).toBeTruthy();
  });

  expect(getPlatformTabPanel('create').getAttribute('aria-hidden')).toBe(
    'false',
  );
  expect(getPlatformTabPanel('home').getAttribute('aria-hidden')).toBe('true');
});

test('save tab can resume a selected archive directly into the game', async () => {
  const user = userEvent.setup();
  const handleContinueGame = vi.fn();

  vi.mocked(listProfileSaveArchives).mockResolvedValue([
    {
      worldKey: 'custom:world-1',
      ownerUserId: null,
      profileId: 'world-1',
      worldType: 'CUSTOM',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '回到旧灯塔继续推进调查。',
      coverImageSrc: null,
      lastPlayedAt: '2026-04-19T12:00:00.000Z',
    },
  ]);
  vi.mocked(resumeProfileSaveArchive).mockResolvedValue({
    entry: {
      worldKey: 'custom:world-1',
      ownerUserId: null,
      profileId: 'world-1',
      worldType: 'CUSTOM',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '回到旧灯塔继续推进调查。',
      coverImageSrc: null,
      lastPlayedAt: '2026-04-19T12:00:00.000Z',
    },
    snapshot: {
      version: 2,
      savedAt: '2026-04-19T12:00:00.000Z',
      bottomTab: 'adventure',
      currentStory: null,
      gameState: {
        worldType: 'CUSTOM',
      },
    } as HydratedSavedGameSnapshot,
  });

  render(<TestWrapper withAuth onContinueGame={handleContinueGame} />);

  await openProfilePlayedWorks(user);

  await clickFirstAsyncButtonByName(user, /潮雾列岛/u);

  await waitFor(() => {
    expect(resumeProfileSaveArchive).toHaveBeenCalledWith('custom:world-1');
    expect(handleContinueGame).toHaveBeenCalledTimes(1);
  });
});

test('profile page keeps save archives inside played stats panel', async () => {
  const user = userEvent.setup();
  const handleContinueGame = vi.fn();

  vi.mocked(listProfileSaveArchives).mockResolvedValue([
    {
      worldKey: 'custom:world-1',
      ownerUserId: null,
      profileId: 'world-1',
      worldType: 'CUSTOM',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '回到旧灯塔继续推进调查。',
      coverImageSrc: null,
      lastPlayedAt: '2026-04-19T12:00:00.000Z',
    },
  ]);
  vi.mocked(resumeProfileSaveArchive).mockResolvedValue({
    entry: {
      worldKey: 'custom:world-1',
      ownerUserId: null,
      profileId: 'world-1',
      worldType: 'CUSTOM',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '回到旧灯塔继续推进调查。',
      coverImageSrc: null,
      lastPlayedAt: '2026-04-19T12:00:00.000Z',
    },
    snapshot: {
      version: 2,
      savedAt: '2026-04-19T12:00:00.000Z',
      bottomTab: 'adventure',
      currentStory: null,
      gameState: {
        worldType: 'CUSTOM',
      },
    } as HydratedSavedGameSnapshot,
  });

  render(<TestWrapper withAuth onContinueGame={handleContinueGame} />);

  await openProfilePlayedWorks(user);

  const continueSectionLabel = screen.getByText('可继续');
  expect(continueSectionLabel.className).toContain('tracking-[0.18em]');
  expect(continueSectionLabel.className).toContain(
    'text-[var(--platform-text-soft)]',
  );
  expect(screen.queryByLabelText('关闭存档')).toBeNull();
  expect(screen.queryByText('SAVES')).toBeNull();
  await clickFirstAsyncButtonByName(user, /潮雾列岛/u);

  await waitFor(() => {
    expect(resumeProfileSaveArchive).toHaveBeenCalledWith('custom:world-1');
    expect(handleContinueGame).toHaveBeenCalledTimes(1);
  });
});

test('creation hub published work can open detail view before deleting from detail page', async () => {
  const user = userEvent.setup();

  const publishedWork = {
    workId: 'published:world-delete-1',
    sourceType: 'published_profile' as const,
    status: 'published' as const,
    title: '潮雾列岛',
    subtitle: '旧灯塔与失控航路',
    summary: '用于测试删除流程的作品。',
    coverImageSrc: null,
    coverRenderMode: 'image' as const,
    coverCharacterImageSrcs: [],
    updatedAt: '2026-04-16T12:00:00.000Z',
    publishedAt: '2026-04-16T12:00:00.000Z',
    stage: null,
    stageLabel: '已发布',
    playableNpcCount: 0,
    landmarkCount: 0,
    roleVisualReadyCount: 0,
    roleAnimationReadyCount: 0,
    roleAssetSummaryLabel: null,
    sessionId: null,
    profileId: 'world-delete-1',
    canResume: false,
    canEnterWorld: true,
  };
  const publishedLibraryEntry = {
    ownerUserId: 'user-1',
    profileId: 'world-delete-1',
    publicWorkCode: 'work-delete-1',
    authorPublicUserCode: 'user-1',
    profile: {
      id: 'world-delete-1',
      name: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '用于测试删除流程的作品。',
      tone: '压抑、潮湿、悬疑',
      playerGoal: '查清旧案。',
      majorFactions: ['守灯会'],
      coreConflicts: ['雾潮正在逼近港口'],
      playableNpcs: [],
      storyNpcs: [],
      landmarks: [],
    } as never,
    visibility: 'published' as const,
    publishedAt: '2026-04-16T12:00:00.000Z',
    updatedAt: '2026-04-16T12:00:00.000Z',
    authorDisplayName: '测试玩家',
    worldName: '潮雾列岛',
    subtitle: '旧灯塔与失控航路',
    summaryText: '用于测试删除流程的作品。',
    coverImageSrc: null,
    themeMode: 'tide' as const,
    playableNpcCount: 0,
    landmarkCount: 0,
    likeCount: 0,
  };

  vi.mocked(listRpgCreationWorks)
    .mockResolvedValueOnce([publishedWork])
    .mockResolvedValue([]);
  vi.mocked(listRpgEntryWorldLibrary)
    .mockResolvedValueOnce([publishedLibraryEntry])
    .mockResolvedValue([]);
  vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /查看详情/u }));

  expect(await screen.findByText('详情')).toBeTruthy();
  expect(screen.getByText('潮雾列岛')).toBeTruthy();
  expect(screen.getByRole('button', { name: '启动' })).toBeTruthy();
  expect(screen.queryByRole('button', { name: '删除作品' })).toBeNull();
  expect(deleteRpgEntryWorldProfile).not.toHaveBeenCalled();
});

test('creation hub published work enters existing detail view', async () => {
  const user = userEvent.setup();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'published:world-public-1',
      sourceType: 'published_profile',
      status: 'published',
      title: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '已经发布的群岛世界作品。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: '2026-04-20T10:00:00.000Z',
      stage: null,
      stageLabel: '已发布',
      playableNpcCount: 3,
      landmarkCount: 4,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: null,
      profileId: 'world-public-1',
      canResume: false,
      canEnterWorld: true,
    },
  ]);
  vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([
    {
      ownerUserId: 'user-1',
      profileId: 'world-public-1',
      publicWorkCode: 'work-public-1',
      authorPublicUserCode: 'user-1',
      profile: {
        id: 'world-public-1',
        name: '潮雾列岛',
        subtitle: '旧灯塔与失控航路',
        summary: '已经发布的群岛世界作品。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清群岛旧案。',
        majorFactions: ['守灯会'],
        coreConflicts: ['假航灯正在扰乱航线'],
        playableNpcs: [],
        storyNpcs: [],
        landmarks: [],
      } as never,
      visibility: 'published',
      publishedAt: '2026-04-20T10:00:00.000Z',
      updatedAt: '2026-04-20T10:00:00.000Z',
      authorDisplayName: '测试玩家',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '已经发布的群岛世界作品。',
      coverImageSrc: null,
      themeMode: 'tide',
      playableNpcCount: 3,
      landmarkCount: 4,
      likeCount: 0,
    },
  ]);

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /查看详情/u }));

  expect(await screen.findByText('详情')).toBeTruthy();
  expect(screen.getByRole('button', { name: '启动' })).toBeTruthy();
  expect(screen.getByText('潮雾列岛')).toBeTruthy();
});

test('creation hub published work experience button enters world directly', async () => {
  const user = userEvent.setup();
  const handleCustomWorldSelect = vi.fn();

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: 'published:world-experience-1',
      sourceType: 'published_profile',
      status: 'published',
      title: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '已经发布的群岛世界作品。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: '2026-04-20T10:00:00.000Z',
      stage: null,
      stageLabel: '已发布',
      playableNpcCount: 3,
      landmarkCount: 4,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: null,
      profileId: 'world-experience-1',
      canResume: false,
      canEnterWorld: true,
    },
  ]);
  vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([
    {
      ownerUserId: 'user-1',
      profileId: 'world-experience-1',
      publicWorkCode: 'work-experience-1',
      authorPublicUserCode: 'user-1',
      profile: {
        id: 'world-experience-1',
        name: '潮雾列岛',
        subtitle: '旧灯塔与失控航路',
        summary: '已经发布的群岛世界作品。',
        tone: '压抑、潮湿、悬疑',
        playerGoal: '查清群岛旧案。',
        majorFactions: ['守灯会'],
        coreConflicts: ['假航灯正在扰乱航线'],
        playableNpcs: [],
        storyNpcs: [],
        landmarks: [],
      } as never,
      visibility: 'published',
      publishedAt: '2026-04-20T10:00:00.000Z',
      updatedAt: '2026-04-20T10:00:00.000Z',
      authorDisplayName: '测试玩家',
      worldName: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summaryText: '已经发布的群岛世界作品。',
      coverImageSrc: null,
      themeMode: 'tide',
      playableNpcCount: 3,
      landmarkCount: 4,
      likeCount: 0,
    },
  ]);

  render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /查看详情/u }));
  await user.click(await screen.findByRole('button', { name: '启动' }));

  await waitFor(() => {
    expect(handleCustomWorldSelect).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 'world-experience-1',
        name: '潮雾列岛',
      }),
    );
  });
  expect(handleCustomWorldSelect).toHaveBeenCalledTimes(1);
});

test('creation hub published work start uses loaded detail profile instead of library summary', async () => {
  const user = userEvent.setup();
  const handleCustomWorldSelect = vi.fn();
  const workProfileId = 'world-detail-launch-1';
  const summaryEntry = buildMockRpgGalleryDetail({
    ownerUserId: mockAuthUser.id,
    profileId: workProfileId,
    publicWorkCode: 'work-detail-launch-1',
    authorPublicUserCode: mockAuthUser.publicUserCode,
    visibility: 'published',
    publishedAt: '2026-04-20T10:00:00.000Z',
    updatedAt: '2026-04-20T10:00:00.000Z',
    authorDisplayName: mockAuthUser.displayName,
    worldName: '星砂废都',
    subtitle: '坠星沙海与废都钟楼',
    summaryText: '列表摘要只提供卡片信息，不能作为运行态 profile。',
    coverImageSrc: null,
    themeMode: 'tide',
    playableNpcCount: 1,
    landmarkCount: 1,
    likeCount: 0,
  });
  summaryEntry.profile = {
    ...summaryEntry.profile,
    name: '默认档案',
    summary: '列表摘要不含运行态角色。',
    playableNpcs: [],
    storyNpcs: [],
    landmarks: [],
  };
  const detailEntry = buildMockRpgGalleryDetail({
    ...summaryEntry,
    summaryText: '详情接口返回完整草稿内容。',
  });
  detailEntry.profile = {
    ...detailEntry.profile,
    name: '星砂废都',
    subtitle: '坠星沙海与废都钟楼',
    summary: '详情接口返回完整草稿内容。',
    playableNpcs: [
      {
        ...compiledAgentResultPreview.playableNpcs[0]!,
        id: 'playable-stardust-1',
        name: '砂眠',
        title: '废都引路人',
      },
    ],
    landmarks: [
      {
        ...compiledAgentResultPreview.landmarks[0]!,
        id: 'landmark-stardust-1',
        name: '坠星钟楼',
      },
    ],
  };

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: `published:${workProfileId}`,
      sourceType: 'published_profile',
      status: 'published',
      title: '星砂废都',
      subtitle: '坠星沙海与废都钟楼',
      summary: '详情接口返回完整草稿内容。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: '2026-04-20T10:00:00.000Z',
      stage: null,
      stageLabel: '已发布',
      playableNpcCount: 1,
      landmarkCount: 1,
      roleVisualReadyCount: 1,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: null,
      profileId: workProfileId,
      canResume: false,
      canEnterWorld: true,
    },
  ]);
  vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([summaryEntry]);
  vi.mocked(
    rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
  ).mockResolvedValue(detailEntry);

  render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /查看详情/u }));
  await waitFor(() => {
    expect(
      rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
    ).toHaveBeenCalledWith(workProfileId);
  });
  await user.click(await screen.findByRole('button', { name: '启动' }));

  await waitFor(() => {
    expect(handleCustomWorldSelect).toHaveBeenCalledWith(
      expect.objectContaining({
        id: workProfileId,
        name: '星砂废都',
        summary: '详情接口返回完整草稿内容。',
        playableNpcs: [
          expect.objectContaining({
            id: 'playable-stardust-1',
            name: '砂眠',
          }),
        ],
        landmarks: [
          expect.objectContaining({
            id: 'landmark-stardust-1',
            name: '坠星钟楼',
          }),
        ],
      }),
    );
  });
  expect(handleCustomWorldSelect).toHaveBeenCalledTimes(1);
});

test('creation hub published work edit keeps loaded detail profile assets instead of library summary', async () => {
  const user = userEvent.setup();
  const workProfileId = 'world-detail-edit-assets-1';
  const summaryEntry = buildMockRpgGalleryDetail({
    ownerUserId: mockAuthUser.id,
    profileId: workProfileId,
    publicWorkCode: 'work-detail-edit-assets-1',
    authorPublicUserCode: mockAuthUser.publicUserCode,
    visibility: 'published',
    publishedAt: '2026-04-20T10:00:00.000Z',
    updatedAt: '2026-04-20T10:00:00.000Z',
    authorDisplayName: mockAuthUser.displayName,
    worldName: '星砂废都',
    subtitle: '坠星沙海与废都钟楼',
    summaryText: '列表摘要字段齐全但不含详情资产。',
    coverImageSrc: null,
    themeMode: 'tide',
    playableNpcCount: 1,
    landmarkCount: 1,
    likeCount: 0,
  });
  summaryEntry.profile = {
    ...summaryEntry.profile,
    name: '星砂废都',
    summary: '列表摘要字段齐全但不含详情资产。',
    playableNpcs: [
      {
        ...compiledAgentResultPreview.playableNpcs[0]!,
        id: 'playable-stardust-1',
        name: '砂眠',
        imageSrc: undefined,
      },
    ],
    storyNpcs: [
      {
        ...compiledAgentResultPreview.storyNpcs[0]!,
        id: 'story-clock-keeper-1',
        name: '钟守',
        imageSrc: undefined,
      },
    ],
    landmarks: [
      {
        ...compiledAgentResultPreview.landmarks[0]!,
        id: 'landmark-stardust-1',
        name: '坠星钟楼',
        imageSrc: undefined,
      },
    ],
    sceneChapterBlueprints: [
      {
        id: 'scene-chapter-stardust-1',
        sceneId: 'landmark-stardust-1',
        title: '坠星钟楼',
        summary: '星砂覆盖钟楼入口，钟守等待第一位访客。',
        sceneTaskDescription: '调查钟楼旧铃自鸣的原因。',
        linkedThreadIds: [],
        linkedLandmarkIds: ['landmark-stardust-1'],
        acts: [
          {
            id: 'act-stardust-opening-1',
            sceneId: 'landmark-stardust-1',
            title: '第一幕',
            summary: '砂眠带玩家进入坠星钟楼。',
            stageCoverage: ['opening'],
            backgroundImageSrc: undefined,
            encounterNpcIds: ['playable-stardust-1'],
            primaryNpcId: 'playable-stardust-1',
            oppositeNpcId: 'story-clock-keeper-1',
            eventDescription: '钟楼旧铃忽然自鸣。',
            linkedThreadIds: [],
            advanceRule: 'after_primary_contact',
            actGoal: '进入钟楼。',
            transitionHook: '星砂开始倒流。',
          },
        ],
      },
    ],
    cover: null,
    openingCg: null,
  };
  const detailEntry = buildMockRpgGalleryDetail({
    ...summaryEntry,
    summaryText: '详情接口返回完整草稿内容。',
  });
  detailEntry.profile = {
    ...summaryEntry.profile,
    summary: '详情接口返回完整草稿内容。',
    cover: {
      sourceType: 'generated',
      imageSrc: '/assets/custom-world/star-waste-cover.png',
      characterRoleIds: ['playable-stardust-1'],
    },
    openingCg: {
      id: 'opening-cg-stardust-1',
      status: 'ready',
      storyboardImageSrc: '/assets/custom-world/opening-storyboard.png',
      videoSrc: '/assets/custom-world/opening.mp4',
      imageModel: 'gpt-image-2',
      videoModel: 'doubao-seedance-2-0-fast-260128',
      aspectRatio: '16:9',
      imageSize: '2k',
      videoResolution: '480p',
      durationSeconds: 15,
      pointCost: 80,
      estimatedWaitMinutes: 10,
      updatedAt: '2026-05-21T00:00:00.000Z',
    },
    camp: {
      id: 'camp-stardust-1',
      name: '废都营地',
      description: '钟楼阴影下的临时营地。',
      imageSrc: '/assets/custom-world/star-waste-camp.png',
      sceneNpcIds: ['playable-stardust-1'],
      connections: [],
    },
    playableNpcs: [
      {
        ...summaryEntry.profile.playableNpcs[0]!,
        imageSrc: '/assets/custom-world/playable-stardust-1.png',
      },
    ],
    storyNpcs: [
      {
        ...summaryEntry.profile.storyNpcs[0]!,
        imageSrc: '/assets/custom-world/story-clock-keeper-1.png',
      },
    ],
    landmarks: [
      {
        ...summaryEntry.profile.landmarks[0]!,
        imageSrc: '/assets/custom-world/landmark-stardust-1.png',
      },
    ],
    sceneChapterBlueprints: [
      {
        ...summaryEntry.profile.sceneChapterBlueprints![0]!,
        acts: [
          {
            ...summaryEntry.profile.sceneChapterBlueprints![0]!.acts[0]!,
            backgroundImageSrc:
              '/assets/custom-world/act-stardust-opening-1.png',
          },
        ],
      },
    ],
  };

  vi.mocked(listRpgCreationWorks).mockResolvedValue([
    {
      workId: `published:${workProfileId}`,
      sourceType: 'published_profile',
      status: 'published',
      title: '星砂废都',
      subtitle: '坠星沙海与废都钟楼',
      summary: '列表摘要字段齐全但不含详情资产。',
      coverImageSrc: null,
      coverRenderMode: 'image',
      coverCharacterImageSrcs: [],
      updatedAt: '2026-04-20T10:00:00.000Z',
      publishedAt: '2026-04-20T10:00:00.000Z',
      stage: null,
      stageLabel: '已发布',
      playableNpcCount: 1,
      landmarkCount: 1,
      roleVisualReadyCount: 0,
      roleAnimationReadyCount: 0,
      roleAssetSummaryLabel: null,
      sessionId: null,
      profileId: workProfileId,
      canResume: false,
      canEnterWorld: true,
    },
  ]);
  vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([summaryEntry]);
  vi.mocked(
    rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
  ).mockResolvedValue(detailEntry);

  render(<TestWrapper withAuth />);

  await openDraftHub(user);
  await user.click(await screen.findByRole('button', { name: /查看详情/u }));
  await waitFor(() => {
    expect(
      rpgEntryLibraryServiceMocks.getRpgEntryWorldLibraryDetail,
    ).toHaveBeenCalledWith(workProfileId);
  });
  await user.click(await screen.findByRole('button', { name: '作品编辑' }));

  expect(
    await screen.findByText('世界档案', {}, { timeout: 5000 }),
  ).toBeTruthy();
  expect(
    document.querySelector('video[src="/assets/custom-world/opening.mp4"]'),
  ).toBeTruthy();

  await user.click(screen.getByRole('tab', { name: /场景\s+2/u }));
  expect((await screen.findByAltText('废都营地')).getAttribute('src')).toBe(
    '/assets/custom-world/star-waste-camp.png',
  );
  expect(screen.getByAltText('坠星钟楼-第一幕').getAttribute('src')).toBe(
    '/assets/custom-world/act-stardust-opening-1.png',
  );

  await user.click(screen.getByRole('tab', { name: /可扮演角色\s+1/u }));
  expect((await screen.findByAltText('砂眠')).getAttribute('src')).toBe(
    '/assets/custom-world/playable-stardust-1.png',
  );

  await user.click(screen.getByRole('tab', { name: /场景角色\s+1/u }));
  expect((await screen.findByAltText('钟守')).getAttribute('src')).toBe(
    '/assets/custom-world/story-clock-keeper-1.png',
  );
});

test('creation hub published work card reveals delete action after card action reveal', async () => {
  const user = userEvent.setup();

  const publishedWork = {
    workId: 'published:world-card-delete-1',
    sourceType: 'published_profile' as const,
    status: 'published' as const,
    title: '潮雾列岛',
    subtitle: '旧灯塔与失控航路',
    summary: '用于测试卡片删除流程的作品。',
    coverImageSrc: null,
    coverRenderMode: 'image' as const,
    coverCharacterImageSrcs: [],
    updatedAt: '2026-04-16T12:00:00.000Z',
    publishedAt: '2026-04-16T12:00:00.000Z',
    stage: null,
    stageLabel: '已发布',
    playableNpcCount: 0,
    landmarkCount: 0,
    roleVisualReadyCount: 0,
    roleAnimationReadyCount: 0,
    roleAssetSummaryLabel: null,
    sessionId: null,
    profileId: 'world-card-delete-1',
    canResume: false,
    canEnterWorld: true,
  };
  const publishedLibraryEntry = {
    ownerUserId: 'user-1',
    profileId: 'world-card-delete-1',
    publicWorkCode: 'work-card-delete-1',
    authorPublicUserCode: 'user-1',
    profile: {
      id: 'world-card-delete-1',
      name: '潮雾列岛',
      subtitle: '旧灯塔与失控航路',
      summary: '用于测试卡片删除流程的作品。',
      tone: '压抑、潮湿、悬疑',
      playerGoal: '查清旧案。',
      majorFactions: ['守灯会'],
      coreConflicts: ['雾潮正在逼近港口'],
      playableNpcs: [],
      storyNpcs: [],
      landmarks: [],
    } as never,
    visibility: 'published' as const,
    publishedAt: '2026-04-16T12:00:00.000Z',
    updatedAt: '2026-04-16T12:00:00.000Z',
    authorDisplayName: '测试玩家',
    worldName: '潮雾列岛',
    subtitle: '旧灯塔与失控航路',
    summaryText: '用于测试卡片删除流程的作品。',
    coverImageSrc: null,
    themeMode: 'tide' as const,
    playableNpcCount: 0,
    landmarkCount: 0,
    likeCount: 0,
  };

  vi.mocked(listRpgCreationWorks)
    .mockResolvedValueOnce([publishedWork])
    .mockResolvedValue([]);
  vi.mocked(listRpgEntryWorldLibrary)
    .mockResolvedValueOnce([publishedLibraryEntry])
    .mockResolvedValue([]);
  vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);

  render(<TestWrapper withAuth />);

  await openDraftHub(user);

  const publishedCard = await screen.findByRole('button', {
    name: /查看详情《潮雾列岛》/u,
  });
  publishedCard.focus();
  await user.keyboard('{ArrowLeft}');

  const deleteButtons = screen.getAllByRole('button', { name: '删除' });
  expect(deleteButtons.length).toBeGreaterThan(0);
  const deleteButton = deleteButtons[0];
  if (!deleteButton) {
    throw new Error('delete button should exist after swipe');
  }
  await user.click(deleteButton);

  const dialog = await screen.findByRole('dialog', { name: '删除作品' });
  expect(dialog.parentElement?.className).toContain('platform-theme--light');
  expect(dialog.parentElement?.className).toContain('!items-center');
  expect(dialog.className).toContain('platform-modal-shell');
  expect(dialog.className).toContain('platform-remap-surface');
  expect(dialog.className).toContain('rounded-[1.75rem]');
  expect(within(dialog).getByText('确认删除《潮雾列岛》吗？')).toBeTruthy();
  expect(within(dialog).getByRole('button', { name: '确认删除' })).toBeTruthy();
  expect(deleteRpgEntryWorldProfile).not.toHaveBeenCalled();
});

test('creation hub gives jump hop wooden fish and bark battle cards the shared delete interaction', async () => {
  const user = userEvent.setup();
  const jumpHopWork = {
    ...buildMockJumpHopWork({
      summary: {
        runtimeKind: 'jump-hop',
        workId: 'jump-hop-work-delete',
        profileId: 'jump-hop-profile-delete',
        ownerUserId: 'user-1',
        sourceSessionId: 'jump-hop-session-delete',
        workTitle: '跳台删除草稿',
        workDescription: '跳一跳草稿也应接入统一删除。',
        themeText: '跳台',
        themeTags: ['跳台'],
        difficulty: 'standard',
        stylePreset: 'paper-toy',
        coverImageSrc: null,
        publicationStatus: 'draft',
        playCount: 0,
        updatedAt: '2026-05-21T10:20:00.000Z',
        publishedAt: null,
        publishReady: true,
        generationStatus: 'ready',
      },
    }).summary,
  } satisfies JumpHopWorkSummaryResponse;
  const woodenFishWork = {
    runtimeKind: 'wooden-fish',
    workId: 'wooden-fish-work-delete',
    profileId: 'wooden-fish-profile-delete',
    ownerUserId: 'user-1',
    sourceSessionId: 'wooden-fish-session-delete',
    workTitle: '木鱼删除草稿',
    workDescription: '敲木鱼草稿也应接入统一删除。',
    themeTags: ['木鱼'],
    coverImageSrc: null,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-21T10:10:00.000Z',
    publishedAt: null,
    publishReady: true,
    generationStatus: 'ready',
  } satisfies WoodenFishWorkSummaryResponse;
  const barkBattleWork = buildMockBarkBattleWork({
    workId: 'bark-battle-work-delete',
    draftId: 'bark-battle-draft-delete',
    title: '声浪删除已发布',
    summary: '汪汪声浪已发布作品也应接入统一删除。',
    updatedAt: '2026-05-21T10:00:00.000Z',
    publishedAt: '2026-05-21T10:00:00.000Z',
  });

  vi.mocked(fetchCreationEntryConfig).mockResolvedValueOnce({
    ...testCreationEntryConfig,
    creationTypes: [
      ...testCreationEntryConfig.creationTypes,
      {
        id: 'jump-hop',
        title: '跳一跳',
        subtitle: '俯视角跳台挑战',
        badge: '可创建',
        imageSrc: '/creation-type-references/jump-hop.webp',
        visible: true,
        open: true,
        sortOrder: 46,
        categoryId: 'recommended',
        categoryLabel: '热门推荐',
        categorySortOrder: 20,
        updatedAtMicros: 1,
      },
      {
        id: 'wooden-fish',
        title: '敲木鱼',
        subtitle: '功德敲击小游戏',
        badge: '可创建',
        imageSrc: '/creation-type-references/wooden-fish.webp',
        visible: true,
        open: true,
        sortOrder: 47,
        categoryId: 'recommended',
        categoryLabel: '热门推荐',
        categorySortOrder: 20,
        updatedAtMicros: 1,
      },
    ],
  });
  vi.mocked(jumpHopClient.listWorks).mockResolvedValue({
    items: [jumpHopWork],
  });
  vi.mocked(woodenFishClient.listWorks).mockResolvedValue({
    items: [woodenFishWork],
  });
  vi.mocked(listBarkBattleWorks).mockResolvedValue({
    items: [barkBattleWork],
  });
  vi.mocked(jumpHopClient.deleteWork).mockResolvedValueOnce({ items: [] });
  vi.mocked(woodenFishClient.deleteWork).mockResolvedValueOnce({ items: [] });
  vi.mocked(deleteBarkBattleWork).mockResolvedValueOnce({ items: [] });

  render(<TestWrapper withAuth />);

  await openDraftHub(user);

  async function revealAndConfirmDelete(
    cardName: RegExp,
    title: string,
  ): Promise<void> {
    const card = await screen.findByRole('button', { name: cardName });
    card.focus();
    await user.keyboard('{ArrowLeft}');
    const shell = card.closest('.creation-work-card-shell');
    if (!shell) {
      throw new Error('作品卡应位于统一操作壳内');
    }
    await user.click(
      within(shell as HTMLElement).getByRole('button', { name: '删除' }),
    );

    const dialog = await screen.findByRole('dialog', { name: '删除作品' });
    expect(within(dialog).getByText(`确认删除《${title}》吗？`)).toBeTruthy();
    await user.click(within(dialog).getByRole('button', { name: '确认删除' }));
  }

  await revealAndConfirmDelete(/继续创作《跳台删除草稿》/u, '跳台删除草稿');
  await waitFor(() => {
    expect(jumpHopClient.deleteWork).toHaveBeenCalledWith(
      'jump-hop-profile-delete',
    );
  });

  await revealAndConfirmDelete(/继续创作《木鱼删除草稿》/u, '木鱼删除草稿');
  await waitFor(() => {
    expect(woodenFishClient.deleteWork).toHaveBeenCalledWith(
      'wooden-fish-profile-delete',
    );
  });

  await revealAndConfirmDelete(/查看详情《声浪删除已发布》/u, '声浪删除已发布');
  await waitFor(() => {
    expect(deleteBarkBattleWork).toHaveBeenCalledWith(
      'bark-battle-work-delete',
    );
  });
});
