/* @vitest-environment jsdom */

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

import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import {
  createCustomWorldAgentSession,
  executeCustomWorldAgentAction,
  getCustomWorldAgentOperation,
  getCustomWorldAgentSession,
  listCustomWorldWorks,
  streamCustomWorldAgentMessage,
} from '../../services/aiService';
import type { AuthUser } from '../../services/authService';
import {
  clearProfileBrowseHistory,
  deleteCustomWorldProfile,
  getCustomWorldGalleryDetail,
  getProfileDashboard,
  listCustomWorldGallery,
  listCustomWorldLibrary,
  listProfileBrowseHistory,
  listProfileSaveArchives,
  resumeProfileSaveArchive,
  upsertCustomWorldProfile,
  upsertProfileBrowseHistory,
} from '../../services/storageService';
import type { GameState } from '../../types';
import {
  AuthUiContext,
  type PlatformSettingsSection,
} from '../auth/AuthUiContext';
import {
  PreGameSelectionFlow,
  type SelectionStage,
} from './PreGameSelectionFlow';

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('创作中心')).toBeTruthy();
}

async function openNewRpgCreation(
  user: ReturnType<typeof userEvent.setup>,
) {
  await openCreationHub(user);
  const createButtons = await screen.findAllByRole('button', {
    name: /新建作品/u,
  });
  await user.click(createButtons.at(-1)!);
  expect(screen.getByText('选择创作类型')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
}

vi.mock('../../services/aiService', () => ({
  createCustomWorldAgentSession: vi.fn(),
  executeCustomWorldAgentAction: vi.fn(),
  generateCustomWorldProfile: vi.fn(),
  getCustomWorldAgentOperation: vi.fn(),
  getCustomWorldAgentSession: vi.fn(),
  listCustomWorldWorks: vi.fn(),
  streamCustomWorldAgentMessage: vi.fn(),
}));

vi.mock('../../services/storageService', () => ({
  clearProfileBrowseHistory: vi.fn(),
  deleteCustomWorldProfile: vi.fn(),
  getCustomWorldGalleryDetail: vi.fn(),
  getProfileDashboard: vi.fn(),
  listCustomWorldGallery: vi.fn(),
  listCustomWorldLibrary: vi.fn(),
  listProfileBrowseHistory: vi.fn(),
  listProfileSaveArchives: vi.fn(),
  publishCustomWorldProfile: vi.fn(),
  resumeProfileSaveArchive: vi.fn(),
  syncProfileBrowseHistory: vi.fn(),
  unpublishCustomWorldProfile: vi.fn(),
  upsertProfileBrowseHistory: vi.fn(),
  upsertCustomWorldProfile: vi.fn(),
}));

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: {
      hook: '被海雾吞没的旧航路群岛。',
      differentiator: '灯塔与禁航令共同决定谁能穿过死潮。',
      desiredExperience: '压抑、潮湿、悬疑',
    },
    playerFantasy: {
      playerRole: '玩家是被迫返乡的守灯人继承者。',
      corePursuit: '查清沉船夜与假航灯的关系。',
      fearOfLoss: '失去家族最后一条可信航线。',
    },
    themeBoundary: {
      toneKeywords: ['压抑', '悬疑'],
      aestheticDirectives: ['潮湿群岛', '冷雾港口'],
      forbiddenDirectives: ['轻喜冒险'],
    },
    playerEntryPoint: {
      openingIdentity: '返乡守灯人继承者',
      openingProblem: '回港首夜撞见禁航区假航灯重亮',
      entryMotivation: '阻止更多船只误入死潮',
    },
    coreConflict: {
      surfaceConflicts: ['守灯会与航运公会争夺航路解释权'],
      hiddenCrisis: '有人在借假航灯持续清洗旧案证据',
      firstTouchedConflict: '玩家返乡当夜就被卷进封航冲突',
    },
    keyRelationships: [
      {
        pairs: '玩家 vs 沈砺',
        relationshipType: '旧友互疑',
        secretOrCost: '他知道沉船夜的另一半真相',
      },
    ],
    hiddenLines: {
      hiddenTruths: ['沉船夜与假航灯骗局属于同一操盘链条'],
      misdirectionHints: ['表面像海雾自然失控'],
      revealPacing: '先见异常，再见旧案，再见操盘者',
    },
    iconicElements: {
      iconicMotifs: ['假航灯', '沉钟回响'],
      institutionsOrArtifacts: ['旧灯塔', '禁航碑'],
      hardRules: ['错误航灯会把船引进必死水域'],
    },
  },
  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: '测试玩家',
  phoneNumberMasked: null,
  loginMethod: 'password',
  bindingStatus: 'active',
  wechatBound: false,
};

type TestAuthValue = {
  user: AuthUser | null;
  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,
    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,
}: {
  withAuth?: boolean;
  authValue?: TestAuthValue;
  onContinueGame?: (snapshot?: HydratedSavedGameSnapshot | null) => void;
} = {}) {
  const [selectionStage, setSelectionStage] =
    useState<SelectionStage>('platform');

  const content = (
    <PreGameSelectionFlow
      selectionStage={selectionStage}
      setSelectionStage={setSelectionStage}
      gameState={{} as GameState}
      hasSavedGame={false}
      savedSnapshot={null}
      handleContinueGame={onContinueGame ?? (() => {})}
      handleStartNewGame={() => {}}
      handleCustomWorldSelect={() => {}}
    />
  );

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

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

beforeEach(() => {
  vi.clearAllMocks();
  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(listCustomWorldLibrary).mockResolvedValue([]);
  vi.mocked(listCustomWorldGallery).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 GameState,
    } as HydratedSavedGameSnapshot,
  });
  vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
  vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
  vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
  vi.mocked(upsertCustomWorldProfile).mockResolvedValue({
    entry: {
      ownerUserId: 'user-1',
      profileId: 'agent-draft-custom-world-agent-session-1',
      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(createCustomWorldAgentSession).mockResolvedValue({
    session: mockSession,
  });
  vi.mocked(listCustomWorldWorks).mockResolvedValue([]);
  vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
    operation: {
      operationId: 'operation-draft-foundation-1',
      type: 'draft_foundation',
      status: 'queued',
      phaseLabel: '已接收请求',
      phaseDetail: '正在准备生成世界底稿。',
      progress: 10,
      error: null,
    },
  });
  vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'running',
    phaseLabel: '生成世界底稿',
    phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
    progress: 38,
    error: null,
  });
  vi.mocked(getCustomWorldAgentSession).mockResolvedValue(mockSession);
  vi.mocked(streamCustomWorldAgentMessage).mockResolvedValue(mockSession);
});

test('create tab opens game type modal, keeps AIRP and visual novel locked, and enters agent workspace for RPG', async () => {
  const user = userEvent.setup();

  render(<TestWrapper />);

  await openCreationHub(user);
  const createButtons = await screen.findAllByRole('button', {
    name: /新建作品/u,
  });
  await user.click(createButtons.at(-1)!);

  expect(screen.getByText('选择创作类型')).toBeTruthy();

  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(createCustomWorldAgentSession).toHaveBeenCalledTimes(1);
  });

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

test('create tab uses unified creation hub and can resume an agent draft', async () => {
  const user = userEvent.setup();

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

  render(<TestWrapper withAuth />);

  await openCreationHub(user);

  expect(
    screen.getByRole('button', { name: /继续精修/u }),
  ).toBeTruthy();
  expect(screen.getByRole('button', { name: /继续精修/u })).toBeTruthy();

  await user.click(screen.getByRole('button', { name: /继续精修/u }));

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

test('clicking a public work while logged out routes through requireAuth', async () => {
  const user = userEvent.setup();
  const requireAuth = vi.fn();

  vi.mocked(listCustomWorldGallery).mockResolvedValue([
    {
      ownerUserId: 'author-1',
      profileId: 'world-public-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(getCustomWorldGalleryDetail).not.toHaveBeenCalled();
});

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(createCustomWorldAgentSession).not.toHaveBeenCalled();
});

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

  render(<TestWrapper />);

  await openNewRpgCreation(user);

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

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

  await waitFor(() => {
    expect(executeCustomWorldAgentAction).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('existing draft sessions enter the agent preview layout without opening legacy editor', async () => {
  const user = userEvent.setup();

  vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
    operationId: 'operation-draft-foundation-1',
    type: 'draft_foundation',
    status: 'completed',
    phaseLabel: '世界底稿已生成',
    phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
    progress: 100,
    error: null,
  });
  vi.mocked(getCustomWorldAgentSession).mockResolvedValue({
    ...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,
      },
    ],
  });

  render(<TestWrapper />);

  await openNewRpgCreation(user);

  await waitFor(
    async () => {
      expect(await screen.findByText('世界档案')).toBeTruthy();
      expect(screen.getByText('已自动保存')).toBeTruthy();
      expect(screen.getByRole('button', { name: /进入世界/u })).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();
  expect(screen.queryByText(/编辑场景角色：顾潮音/u)).toBeNull();
  expect(screen.queryByRole('button', { name: /AI生成/u })).toBeNull();
  expect(screen.queryByText('技能')).toBeNull();
});

test('agent draft result back button returns to workspace without redundant sync when session is already latest', async () => {
  const user = userEvent.setup();

  vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
    operation: {
      operationId: 'operation-sync-result-profile-1',
      type: 'sync_result_profile',
      status: 'queued',
      phaseLabel: '同步结果页快照',
      phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
      progress: 24,
      error: null,
    },
  });
  vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
    operationId: 'operation-sync-result-profile-1',
    type: 'sync_result_profile',
    status: 'completed',
    phaseLabel: '结果页快照已同步',
    phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
    progress: 100,
    error: null,
  });

  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,
      },
    ],
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getCustomWorldAgentSession).mockResolvedValue(resultSession);

  render(<TestWrapper />);

  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('Agent工作区：custom-world-agent-session-1'),
    ).toBeTruthy();
  });

  expect(
    vi.mocked(executeCustomWorldAgentAction).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 persists the latest profile rebuilt from synced session', async () => {
  const user = userEvent.setup();

  vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
    operation: {
      operationId: 'operation-sync-result-profile-2',
      type: 'sync_result_profile',
      status: 'queued',
      phaseLabel: '同步结果页快照',
      phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
      progress: 24,
      error: null,
    },
  });
  vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
    operationId: 'operation-sync-result-profile-2',
    type: 'sync_result_profile',
    status: 'completed',
    phaseLabel: '结果页快照已同步',
    phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
    progress: 100,
    error: null,
  });

  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,
      },
    ],
  } satisfies CustomWorldAgentSessionSnapshot;
  vi.mocked(getCustomWorldAgentSession).mockResolvedValue(syncedSession);

  render(<TestWrapper />);

  await openNewRpgCreation(user);

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

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

  const latestSavedProfile = vi.mocked(upsertCustomWorldProfile).mock.calls.at(-1)?.[0];
  expect(latestSavedProfile?.name).toBe('潮雾列岛·session最新版');
  expect(latestSavedProfile?.summary).toBe(
    '作品库应该保存这份同步后的最新快照。',
  );
});

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('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 GameState,
    } as HydratedSavedGameSnapshot,
  });

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

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

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

test('owned world detail can delete a work and return to the create tab list', 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',
    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(listCustomWorldWorks)
    .mockResolvedValueOnce([publishedWork])
    .mockResolvedValue([]);
  vi.mocked(listCustomWorldLibrary)
    .mockResolvedValueOnce([publishedLibraryEntry])
    .mockResolvedValue([]);
  vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);

  render(<TestWrapper withAuth />);

  await openCreationHub(user);
  await user.click(screen.getByRole('button', { name: /进入世界/u }));
  await user.click(await screen.findByRole('button', { name: '删除作品' }));

  await waitFor(() => {
    expect(deleteCustomWorldProfile).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(listCustomWorldWorks).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(listCustomWorldLibrary).mockResolvedValue([
    {
      ownerUserId: 'user-1',
      profileId: 'world-public-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(screen.getByRole('button', { name: /进入世界/u }));

  expect(await screen.findByText('世界信息')).toBeTruthy();
  expect(screen.getByRole('button', { name: '开始游戏' })).toBeTruthy();
  expect(screen.getByText('已发布')).toBeTruthy();
});
