/* @vitest-environment jsdom */

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

import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import { ApiClientError } from '../../services/apiClient';
import type { AuthUser } from '../../services/authService';
import {
  createBigFishCreationSession,
  getBigFishCreationSession,
} from '../../services/big-fish-creation';
import { listBigFishGallery } from '../../services/big-fish-gallery';
import { startLocalBigFishRuntimeRun } from '../../services/big-fish-runtime';
import { listBigFishWorks } from '../../services/big-fish-works';
import {
  createPuzzleAgentSession,
  getPuzzleAgentSession,
} from '../../services/puzzle-agent';
import {
  getPuzzleGalleryDetail,
  listPuzzleGallery,
} from '../../services/puzzle-gallery';
import { listPuzzleWorks } 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,
  getRpgEntryWorldGalleryDetailByCode,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import {
  AuthUiContext,
  type PlatformSettingsSection,
} from '../auth/AuthUiContext';
import {
  RpgEntryFlowShell,
  type RpgEntryFlowShellProps,
  type SelectionStage,
} from './RpgEntryFlowShell';

async function clickFirstButtonByName(
  user: ReturnType<typeof userEvent.setup>,
  name: string | RegExp,
) {
  const buttons = screen.getAllByRole('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 openCreationHub(user: ReturnType<typeof userEvent.setup>) {
  await clickFirstButtonByName(user, '创作');
  expect(await screen.findByText('角色扮演 RPG')).toBeTruthy();
}

async function openNewRpgCreation(user: ReturnType<typeof userEvent.setup>) {
  await openCreationHub(user);
  await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
}

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

  return panel;
}

vi.mock('../../services/rpg-creation', () => ({
  createRpgCreationSession: vi.fn(),
  executeRpgCreationAction: vi.fn(),
  getRpgCreationOperation: vi.fn(),
  getRpgCreationResultView: vi.fn(),
  getRpgCreationSession: vi.fn(),
  listRpgCreationWorks: vi.fn(),
  streamRpgCreationMessage: vi.fn(),
  upsertRpgWorldProfile: vi.fn(),
}));

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

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

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

vi.mock('../../services/rpg-entry/rpgEntryLibraryClient', () => ({
  deleteRpgEntryWorldProfile: vi.fn(),
  getRpgEntryWorldGalleryDetailByCode: vi.fn(),
}));

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', () => ({
  advanceLocalBigFishRuntimeRun: vi.fn((run) => run),
  startLocalBigFishRuntimeRun: vi.fn(),
}));

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

vi.mock('../puzzle-agent/PuzzleAgentWorkspace', () => ({
  PuzzleAgentWorkspace: ({
    session,
    onBack,
  }: {
    session: { sessionId: string; messages: Array<{ text: string }> } | null;
    onBack: () => void;
  }) => (
    <div className="puzzle-agent-workspace-mock">
      <div>拼图工作区：{session?.sessionId ?? 'missing-session'}</div>
      {session?.messages.map((message) => (
        <div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
      ))}
      <button type="button" onClick={onBack}>
        返回
      </button>
    </div>
  ),
}));

vi.mock('../puzzle-result/PuzzleResultView', () => ({
  PuzzleResultView: ({
    session,
    onBack,
  }: {
    session: { draft?: { levelName: string } | null };
    onBack: () => void;
  }) => (
    <div className="puzzle-result-view-mock">
      <div>拼图结果页</div>
      <label>
        关卡名
        <input readOnly value={session.draft?.levelName ?? ''} />
      </label>
      <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('../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',
  username: 'tester',
  displayName: '测试玩家',
  publicUserCode: 'user-tester',
  phoneNumberMasked: null,
  loginMethod: 'password',
  bindingStatus: 'active',
  wechatBound: false,
};

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,
  },
};

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,
  };
}

type TestAuthValue = {
  user: AuthUser | null;
  canAccessProtectedData: boolean;
  openLoginModal: (postLoginAction?: (() => void) | null) => void;
  requireAuth: (action: () => void) => void;
  openSettingsModal: (section?: PlatformSettingsSection) => void;
  openAccountModal: () => 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: () => {},
    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>(() =>
    window.location.pathname === '/creation/rpg/agent'
      ? 'agent-workspace'
      : 'platform',
  );

  const content = (
    <RpgEntryFlowShell
      selectionStage={selectionStage}
      setSelectionStage={setSelectionStage}
      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();
  window.history.replaceState(null, '', '/');
  window.sessionStorage.clear();
  window.localStorage.clear();
  vi.mocked(getProfileDashboard).mockResolvedValue({
    walletBalance: 0,
    totalPlayTimeMs: 0,
    playedWorldCount: 0,
    updatedAt: '2026-04-16T12:00:00.000Z',
  });
  vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([]);
  vi.mocked(listRpgEntryWorldGallery).mockResolvedValue([]);
  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(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,
    },
    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(startLocalBigFishRuntimeRun).mockReturnValue({
    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(listPuzzleWorks).mockResolvedValue({
    items: [],
  });
  vi.mocked(listPuzzleGallery).mockResolvedValue({
    items: [],
  });
  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);
});

test('create hub exposes direct template entry, keeps AIRP and visual novel locked, and enters agent workspace for RPG', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreationHub(user);

  const airpButton = screen.getByRole('button', { name: /AIRP/u });
  const visualNovelButton = screen.getByRole('button', {
    name: /视觉小说/u,
  });

  expect((airpButton as HTMLButtonElement).disabled).toBe(true);
  expect((visualNovelButton as HTMLButtonElement).disabled).toBe(true);

  await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));

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

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

test('platform create hub does not prefetch hidden big fish platform data', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreationHub(user);

  expect(
    await screen.findByRole('button', { name: /角色扮演 RPG/u }),
  ).toBeTruthy();
  expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
  expect(listBigFishWorks).not.toHaveBeenCalled();
  expect(listBigFishGallery).not.toHaveBeenCalled();
});

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

  render(<TestWrapper withAuth />);

  await openNewRpgCreation(user);

  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);
});

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 openCreationHub(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 openCreationHub(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 openCreationHub(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 create 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 openCreationHub(user);
  await user.click(await screen.findByRole('button', { name: /继续完善/u }));

  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('create')).getByText(
        '这份共创草稿已失效，已为你返回创作中心，请重新开始创作。',
      ),
    ).toBeTruthy();
  });

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

test('clicking a public work while logged out routes through requireAuth', 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,
    },
  ]);

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

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

  expect(requireAuth).toHaveBeenCalledTimes(1);
  expect(getRpgEntryWorldGalleryDetail).not.toHaveBeenCalled();
});

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',
        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,
        publishReady: false,
      },
    ],
  });

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

  await openCreationHub(user);
  const createPanel = getPlatformTabPanel('create');

  expect(await within(createPanel).findByText('RPG 退出缓存作品')).toBeTruthy();
  expect(within(createPanel).queryByText('大鱼退出缓存作品')).toBeNull();
  expect(await within(createPanel).findByText('拼图退出缓存作品')).toBeTruthy();

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

  await waitFor(() => {
    expect(within(createPanel).queryByText('RPG 退出缓存作品')).toBeNull();
    expect(within(createPanel).queryByText('拼图退出缓存作品')).toBeNull();
  });
  expect(within(createPanel).getByText('还没有作品')).toBeTruthy();
});

test('published puzzle works appear on home and category public shelves', 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,
    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 user.click(screen.getByRole('button', { name: '分类' }));

  const categoryPanel = getPlatformTabPanel('category');
  expect(within(categoryPanel).getAllByText('星桥机关').length).toBeGreaterThan(
    0,
  );
  expect(
    within(categoryPanel).getAllByRole('button', { name: /机关/u }).length,
  ).toBeGreaterThan(0);
});

test('published big fish works stay hidden from platform home and category shelves', async () => {
  const user = userEvent.setup();
  const publishedBigFishWork: BigFishWorkSummary = {
    workId: 'big-fish-work-public-1',
    sourceSessionId: 'big-fish-session-public-1',
    ownerUserId: 'user-2',
    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 user.click(screen.getByRole('button', { name: '分类' }));

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

test('published puzzle detail returns to the source 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,
    publishReady: true,
  } satisfies PuzzleWorkSummary;

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

  render(<TestWrapper withAuth />);

  await user.click(await screen.findByRole('button', { name: '分类' }));
  await waitFor(() => {
    expect(document.getElementById('platform-tab-panel-category')).toBeTruthy();
  });
  await waitFor(() => {
    const categoryPanel = getPlatformTabPanel('category');
    expect(
      within(categoryPanel).getAllByText('星桥机关').length,
    ).toBeGreaterThan(0);
  });
  const categoryPanel = getPlatformTabPanel('category');

  await user.click(
    within(categoryPanel).getByRole('button', {
      name: /拼图关卡.*星桥机关/u,
    }),
  );
  expect(
    await screen.findByRole('button', { name: '进入第 1 关' }),
  ).toBeTruthy();

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

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

test('selecting RPG creation while logged out routes through requireAuth', async () => {
  const user = userEvent.setup();
  const requireAuth = vi.fn();

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

  await openNewRpgCreation(user);
  expect(requireAuth).toHaveBeenCalledTimes(1);
  expect(createRpgCreationSession).not.toHaveBeenCalled();
});

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('new creation entry maps raw bearer token errors to user-facing auth copy', async () => {
  const user = userEvent.setup();

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

  render(<TestWrapper withAuth />);

  await openCreationHub(user);
  await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));

  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('create')).getByText(
        '当前登录状态已失效，请重新登录后继续。',
      ),
    ).toBeTruthy();
  });

  expect(listPuzzleWorks).toHaveBeenCalled();
  expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
});

test('hidden big fish creation entry does not render in platform create hub', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openCreationHub(user);

  expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
  expect(createBigFishCreationSession).not.toHaveBeenCalled();
});

test('puzzle creation 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 openCreationHub(user);

  const button = screen.getByRole('button', { name: /拼图玩法/u });
  await user.click(button);

  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('create')).getAllByText(
        '开启拼图创作工作台超时，请确认运行时后端已启动后重试。',
      ).length,
    ).toBeGreaterThan(0);
  });
  expect((button as HTMLButtonElement).disabled).toBe(false);
  expect(screen.queryByText(/正在准备拼图共创工作区/u)).toBeNull();
});

test('puzzle draft card restores the bound agent session and opens the result view', 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,
        publishReady: false,
      },
    ],
  });

  render(<TestWrapper withAuth />);

  await openCreationHub(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: '返回' }));

  expect(
    await screen.findByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
  ).toBeTruthy();
  expect(screen.queryByText('拼图玩法共创')).toBeNull();
});

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,
        publishReady: true,
      },
    ],
  });

  render(<TestWrapper withAuth />);

  await openCreationHub(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('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,
    publishReady: true,
  };

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

  render(<TestWrapper withAuth />);

  const searchInput = await screen.findByPlaceholderText(
    '输入 SY / CW / BF / PZ 编号',
  );
  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('进入第 1 关')).toBeTruthy();
  expect(screen.getByText('雨夜猫塔')).toBeTruthy();
  expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
});

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',
    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 />);

  const searchInput = await screen.findByPlaceholderText(
    '输入 SY / CW / BF / PZ 编号',
  );
  await user.type(searchInput, 'BF-NPUBLIC1');
  await user.click(screen.getByRole('button', { name: '搜索' }));

  await waitFor(() => {
    expect(startLocalBigFishRuntimeRun).toHaveBeenCalledWith({
      work: expect.objectContaining({
        sourceSessionId: '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('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
  const user = userEvent.setup();

  render(<TestWrapper withAuth />);

  await openNewRpgCreation(user);

  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.findByText('世界草稿生成进度')).toBeTruthy();
  expect(screen.queryByText(/Agent工作区/u)).toBeNull();
  expect(screen.getAllByText('生成世界底稿').length).toBeGreaterThan(0);
  expect(screen.getByText('当前世界信息')).toBeTruthy();
  expect(screen.queryByText('回到工作区')).toBeNull();
  expect(screen.getByText('世界承诺')).toBeTruthy();
  expect(screen.getByText(/灯塔与禁航令共同决定谁能穿过死潮/u)).toBeTruthy();
  expect(screen.queryByText('先告诉我你想做一个怎样的 RPG 世界。')).toBeNull();
});

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.findByText('世界草稿生成进度')).toBeTruthy();
  expect(screen.queryByText(/Agent工作区/u)).toBeNull();
  expect(screen.getAllByText('生成世界底稿').length).toBeGreaterThan(0);
});

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 openCreationHub(user);
  expect(await screen.findByText('失败中的潮雾列岛')).toBeTruthy();
  await user.click(await screen.findByRole('button', { name: /继续创作/u }));

  expect(await screen.findByText('世界草稿生成进度')).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),
  );

  render(<TestWrapper withAuth />);

  await openNewRpgCreation(user);

  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('button', { 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),
  );

  render(<TestWrapper withAuth />);

  await openNewRpgCreation(user);

  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(createRpgCreationSession).mockResolvedValue({
    session: publishReadyDraftSession,
  });
  vi.mocked(getRpgCreationOperation).mockResolvedValueOnce({
    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 openNewRpgCreation(user);

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

  await waitFor(() => {
    expect(executeRpgCreationAction).toHaveBeenCalledWith(
      'custom-world-agent-session-1',
      expect.objectContaining({
        action: 'publish_world',
      }),
    );
  });
  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: false,
      blockers: [
        {
          id: 'missing-cover-image',
          code: 'MISSING_COVER_IMAGE',
          message: '发布前需要补齐作品封面。',
        },
      ],
    },
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getRpgCreationSession).mockResolvedValue(testDraftSession);
  vi.mocked(getRpgCreationResultView).mockResolvedValue(
    buildResultViewForSession(testDraftSession),
  );

  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 openNewRpgCreation(user);
  await user.click(await screen.findByRole('button', { name: '作品测试' }));

  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);
});

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: {
        ...compiledAgentDraftSession.resultPreview!.preview,
        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 openCreationHub(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 creation 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),
  );

  render(<TestWrapper withAuth />);

  await openNewRpgCreation(user);

  await waitFor(
    async () => {
      expect(await screen.findByText('世界档案')).toBeTruthy();
      expect(screen.getByRole('button', { name: /返回创作/u })).toBeTruthy();
    },
    { timeout: 2500 },
  );

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

  await waitFor(() => {
    expect(screen.getByText('角色扮演 RPG')).toBeTruthy();
  });

  expect(
    vi
      .mocked(executeRpgCreationAction)
      .mock.calls.some(
        ([sessionId, payload]) =>
          sessionId === 'custom-world-agent-session-1' &&
          payload?.action === 'sync_result_profile',
      ),
  ).toBe(false);
  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),
  );
  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 openNewRpgCreation(user);

  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),
  );

  render(<TestWrapper withAuth />);

  await openNewRpgCreation(user);

  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 with save archives default into the saves tab', async () => {
  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 />);

  expect((await screen.findAllByText('全部存档')).length).toBeGreaterThan(0);
  expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
  expect(screen.getAllByText('最近更新时间排序').length).toBeGreaterThan(0);
  expect(screen.queryByText('SAVE ARCHIVE')).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.findByText('角色扮演 RPG')).toBeTruthy();

  resolveGalleryRequest([]);

  await waitFor(() => {
    expect(
      within(getPlatformTabPanel('create')).getByText('角色扮演 RPG'),
    ).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 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();
  vi.spyOn(window, 'confirm').mockReturnValue(true);

  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,
  };

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

  render(<TestWrapper withAuth />);

  await openCreationHub(user);
  await user.click(await screen.findByRole('button', { name: /查看详情/u }));
  await user.click(await screen.findByRole('button', { name: '删除作品' }));

  await waitFor(() => {
    expect(deleteRpgEntryWorldProfile).toHaveBeenCalledWith('world-delete-1');
  });

  await waitFor(() => {
    expect(screen.queryByRole('button', { name: '删除作品' })).toBeNull();
  });
  await waitFor(() => {
    expect(screen.getByText('还没有作品')).toBeTruthy();
  });
});

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,
    },
  ]);

  render(<TestWrapper withAuth />);

  await openCreationHub(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,
    },
  ]);

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

  await openCreationHub(user);
  await user.click(await screen.findByRole('button', { name: '体验' }));

  await waitFor(() => {
    expect(handleCustomWorldSelect).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 'world-experience-1',
        name: '潮雾列岛',
      }),
    );
  });
  expect(screen.queryByText('世界信息')).toBeNull();
});

test('creation hub published work delete button removes the work directly from card list', async () => {
  const user = userEvent.setup();
  vi.spyOn(window, 'confirm').mockReturnValue(true);

  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,
  };

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

  render(<TestWrapper withAuth />);

  await openCreationHub(user);
  await user.click(await screen.findByRole('button', { name: '删除' }));

  await waitFor(() => {
    expect(deleteRpgEntryWorldProfile).toHaveBeenCalledWith(
      'world-card-delete-1',
    );
  });
  await waitFor(() => {
    expect(screen.getByText('还没有作品')).toBeTruthy();
  });
});
