/* @vitest-environment jsdom */

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

import * as customWorldCoverAssetService from '../services/customWorldCoverAssetService';
import * as rpgCreationAssetClient from '../services/rpg-creation/rpgCreationAssetClient';
import type {
  CustomWorldNpc,
  CustomWorldPlayableNpc,
  CustomWorldProfile,
  SceneActBlueprint,
  SceneChapterBlueprint,
} from '../types';
import { CustomWorldEntityCatalog } from './CustomWorldEntityCatalog';
import {
  type RpgCreationEditorTarget,
  RpgCreationEntityEditorModal,
} from './rpg-creation-editor/RpgCreationEntityEditorModal';

afterEach(() => {
  cleanup();
});

vi.mock('../data/characterPresets', async () => {
  const actual = await vi.importActual<
    typeof import('../data/characterPresets')
  >('../data/characterPresets');

  return {
    ...actual,
    buildCustomWorldPlayableCharacters: vi.fn(() => []),
    buildCustomWorldRuntimeCharacters: vi.fn(() => []),
    createCharacterSkillCooldowns: vi.fn(() => ({})),
    getCharacterMaxHp: vi.fn(() => 180),
    getCharacterMaxMana: vi.fn(() => 60),
    setRuntimeCharacterOverrides: vi.fn(),
  };
});

vi.mock('./CharacterAnimator', () => ({
  CharacterAnimator: () => <div>角色预览</div>,
}));

vi.mock('../services/rpg-creation/rpgCreationAssetClient', () => {
  const generateSceneImage = vi.fn();
  const generateSceneNpc = vi.fn();

  return {
    rpgCreationAssetClient: {
      generateSceneImage,
      generateSceneNpc,
    },
    generateCustomWorldSceneImage: generateSceneImage,
    generateCustomWorldSceneNpc: generateSceneNpc,
  };
});

const mockedRpgCreationAssetClient = vi.mocked(
  rpgCreationAssetClient.rpgCreationAssetClient,
);

vi.mock('./CustomWorldNpcVisualEditor', () => ({
  CustomWorldNpcPortrait: ({ npc }: { npc: { name: string } }) => (
    <div>{npc.name}</div>
  ),
  CustomWorldNpcVisualEditor: () => <div>预设形象编辑器</div>,
}));

vi.mock('../hooks/useResolvedAssetReadUrl', () => ({
  useResolvedAssetReadUrl: (source: string | null | undefined) => ({
    resolvedUrl: source?.trim() ?? '',
    isResolving: false,
    shouldResolve: false,
  }),
}));

vi.mock('./rpg-runtime-shell', () => ({
  RpgRuntimeShell: ({
    session,
    chrome,
  }: {
    session: {
      gameState: {
        currentScenePreset?: { id?: string; name?: string } | null;
        playerCharacter?: { name?: string } | null;
        runtimeSessionId?: string | null;
        runtimeMode?: string;
        runtimePersistenceDisabled?: boolean;
      };
      currentStory?: { text?: string } | null;
    };
    chrome?: { hidePlayerLevelBadge?: boolean };
  }) => (
    <div>
      <div>幕预览运行时</div>
      {chrome?.hidePlayerLevelBadge ? <div>隐藏等级徽标</div> : null}
      <div>{session.gameState.currentScenePreset?.name ?? '未进入场景'}</div>
      <div>{session.gameState.currentScenePreset?.id ?? '未进入场景ID'}</div>
      <div>
        {session.gameState.playerCharacter ? '已选择预览角色' : '未选择角色'}
      </div>
      <div>{session.gameState.runtimeSessionId ?? '未设置预览会话'}</div>
      <div>{session.gameState.runtimeMode ?? '未设置运行模式'}</div>
      <div>
        {session.gameState.runtimePersistenceDisabled
          ? '预览禁用持久化'
          : '预览允许持久化'}
      </div>
      <div>{session.currentStory?.text ?? '未生成当前故事'}</div>
    </div>
  ),
}));

vi.mock('./asset-studio/characterAssetWorkflowPersistence', () => ({
  fetchCharacterWorkflowCache: vi.fn().mockResolvedValue({ cache: null }),
  saveCharacterWorkflowCache: vi.fn().mockResolvedValue(undefined),
  resolveCharacterRoleAssetWorkflow: vi.fn(({ role }) =>
    Promise.resolve({
      ok: true,
      cache: null,
      workflow: {
        role,
        defaultPromptBundle: {
          visualPromptText: '',
          animationPromptText: '',
          scenePromptText: '',
        },
        visualPromptText: '',
        animationPromptText: '',
        animationPromptTextByKey: {},
        visualDrafts: [],
        selectedVisualDraftId: '',
        selectedAnimation: 'idle',
      },
    }),
  ),
  putCharacterRoleAssetWorkflow: vi.fn().mockResolvedValue({
    ok: true,
    cache: null,
  }),
  generateCharacterVisualCandidates: vi.fn(),
  publishCharacterVisualAsset: vi.fn(),
  generateCharacterAnimationDraft: vi.fn(),
  publishCharacterAnimationAssets: vi.fn(),
}));

vi.mock('../services/customWorldCoverAssetService', () => ({
  generateCustomWorldCoverImage: vi.fn(),
  uploadCustomWorldCoverImage: vi.fn(),
}));

function createBackstoryReveal() {
  return {
    publicSummary: '公开背景',
    chapters: [
      {
        id: 'surface',
        title: '表层来意',
        affinityRequired: 6,
        teaser: '表层来意',
        content: '表层来意内容',
        contextSnippet: '表层来意摘要',
      },
      {
        id: 'scar',
        title: '旧事裂痕',
        affinityRequired: 12,
        teaser: '旧事裂痕',
        content: '旧事裂痕内容',
        contextSnippet: '旧事裂痕摘要',
      },
      {
        id: 'hidden',
        title: '隐藏执念',
        affinityRequired: 18,
        teaser: '隐藏执念',
        content: '隐藏执念内容',
        contextSnippet: '隐藏执念摘要',
      },
      {
        id: 'final',
        title: '最终底牌',
        affinityRequired: 24,
        teaser: '最终底牌',
        content: '最终底牌内容',
        contextSnippet: '最终底牌摘要',
      },
    ],
  };
}

function createPlayableRole(id: string, name: string): CustomWorldPlayableNpc {
  return {
    id,
    name,
    title: '同行者',
    role: '协作战力',
    description: `${name}的定位描述`,
    backstory: `${name}的背景`,
    personality: `${name}的性格`,
    motivation: `${name}的动机`,
    combatStyle: `${name}的战斗风格`,
    initialAffinity: 18,
    relationshipHooks: ['关系钩子'],
    relations: [],
    tags: ['测试'],
    backstoryReveal: createBackstoryReveal(),
    skills: [],
    initialItems: [],
  };
}

function createStoryRole(id: string, name: string): CustomWorldNpc {
  return {
    ...createPlayableRole(id, name),
    initialAffinity: 6,
    visual: undefined,
  };
}

function createProfile(): CustomWorldProfile {
  return {
    id: 'world-1',
    settingText: '潮雾群岛上的禁制与旧航道正在一起失衡。',
    name: '潮雾群岛',
    subtitle: '旧航道与沉钟回响',
    summary: '一座正在被旧誓与新利益共同撕扯的群岛世界。',
    tone: '压抑、潮湿、带着未解旧伤。',
    playerGoal: '找到能让群岛重新稳定的关键节点。',
    templateWorldType: 'WUXIA',
    majorFactions: ['守潮盟', '沉钟会'],
    coreConflicts: ['旧航道归属', '沉钟遗产争夺'],
    attributeSchema: {
      id: 'schema-1',
      worldId: 'world-1',
      schemaVersion: 1,
      generatedFrom: {
        worldType: 'WUXIA',
        worldName: '潮雾群岛',
        settingSummary: '潮雾群岛上的禁制与旧航道正在一起失衡。',
        tone: '压抑、潮湿、带着未解旧伤。',
        conflictCore: '旧航道归属',
      },
      slots: [
        {
          slotId: 'axis_a',
          name: '骨势',
        },
        {
          slotId: 'axis_b',
          name: '身法',
        },
        {
          slotId: 'axis_c',
          name: '眼脉',
        },
        {
          slotId: 'axis_d',
          name: '心焰',
        },
        {
          slotId: 'axis_e',
          name: '尘缘',
        },
        {
          slotId: 'axis_f',
          name: '玄息',
        },
      ],
    },
    playableNpcs: [createPlayableRole('playable-1', '沈砺')],
    storyNpcs: [createStoryRole('story-1', '顾潮音')],
    items: [],
    camp: {
      name: '潮灯居',
      description: '玩家最初落脚的旧灯塔内院。',
    },
    landmarks: [],
    creatorIntent: null,
    anchorPack: null,
    lockState: null,
    ownedSettingLayers: null,
    generationMode: 'full',
    generationStatus: 'complete',
  } as unknown as CustomWorldProfile;
}

function createProfileWithLandmark(): CustomWorldProfile {
  return {
    ...createProfile(),
    storyNpcs: [
      createStoryRole('story-1', '顾潮音'),
      createStoryRole('story-2', '闻雪汀'),
      createStoryRole('story-3', '谢孤灯'),
      createStoryRole('story-4', '陆听潮'),
    ],
    landmarks: [
      {
        id: 'landmark-1',
        name: '沉钟栈桥',
        description: '旧钟与潮声常年相撞的码头栈桥。',
        imageSrc: '/generated-custom-world-scenes/original-scene.png',
        sceneNpcIds: ['story-1', 'story-2', 'story-3'],
        connections: [],
      },
    ],
  } as unknown as CustomWorldProfile;
}

function createProfileWithTwoLandmarks(): CustomWorldProfile {
  return {
    ...createProfileWithLandmark(),
    landmarks: [
      {
        id: 'landmark-1',
        name: '沉钟栈桥',
        description: '旧钟与潮声常年相撞的码头栈桥。',
        imageSrc: '/generated-custom-world-scenes/original-scene.png',
        sceneNpcIds: ['story-1', 'story-2', 'story-3'],
        connections: [],
      },
      {
        id: 'landmark-2',
        name: '雾灯塔',
        description: '雾中仍在闪烁的旧灯塔。',
        sceneNpcIds: ['story-1', 'story-2', 'story-3'],
        connections: [],
      },
    ],
  } as unknown as CustomWorldProfile;
}

function createSceneAct(
  sceneId: string,
  index: number,
  imageSrc: string,
): SceneActBlueprint {
  return {
    id: `${sceneId}-act-${index + 1}`,
    sceneId,
    title: `第${index + 1}幕`,
    summary: `第${index + 1}幕摘要`,
    stageCoverage: index === 0 ? ['opening'] : ['expansion'],
    backgroundPromptText: '',
    backgroundImageSrc: imageSrc,
    encounterNpcIds: ['story-1'],
    primaryNpcId: 'story-1',
    oppositeNpcId: 'story-1',
    eventDescription: `第${index + 1}幕事件`,
    linkedThreadIds: [],
    advanceRule:
      index === 0
        ? 'after_primary_contact'
        : index >= 2
          ? 'after_chapter_resolution'
          : 'after_active_step_complete',
    actGoal: `第${index + 1}幕目标`,
    transitionHook: '',
  };
}

function createSceneChapter(
  sceneId: string,
  sceneName: string,
  imagePrefix: string,
): SceneChapterBlueprint {
  return {
    id: `${sceneId}-chapter`,
    sceneId,
    title: sceneName,
    summary: `${sceneName}章节`,
    sceneTaskDescription: `${sceneName}任务`,
    linkedThreadIds: [],
    linkedLandmarkIds: [sceneId],
    acts: [0, 1, 2].map((index) =>
      createSceneAct(sceneId, index, `${imagePrefix}-act-${index + 1}.png`),
    ),
  };
}

function createProfileWithSceneChapters(): CustomWorldProfile {
  return {
    ...createProfileWithLandmark(),
    camp: {
      id: 'custom-scene-camp',
      name: '潮灯居',
      description: '玩家最初落脚的旧灯塔内院。',
      imageSrc: '/generated-custom-world-scenes/camp-main.png',
      sceneNpcIds: ['story-1'],
      connections: [],
    },
    sceneChapterBlueprints: [
      createSceneChapter(
        'custom-scene-camp',
        '潮灯居',
        '/generated-custom-world-scenes/camp',
      ),
      createSceneChapter(
        'landmark-1',
        '沉钟栈桥',
        '/generated-custom-world-scenes/landmark',
      ),
    ],
  } as unknown as CustomWorldProfile;
}

function LandmarkEditorFlowHarness() {
  const [profile, setProfile] = useState(createProfileWithLandmark());
  const [target, setTarget] = useState<RpgCreationEditorTarget | null>({
    kind: 'landmark',
    mode: 'edit',
    id: 'landmark-1',
  });

  return (
    <>
      <pre data-testid="landmark-profile-json" className="hidden">
        {JSON.stringify(profile)}
      </pre>
      <CustomWorldEntityCatalog
        profile={profile}
        previewCharacters={[]}
        activeTab="landmarks"
        onActiveTabChange={() => {}}
        onEditTarget={setTarget}
        onProfileChange={setProfile}
        onDeleteStoryNpcs={() => {}}
        onDeleteLandmarks={() => {}}
      />
      <RpgCreationEntityEditorModal
        profile={profile}
        target={target}
        onClose={() => setTarget(null)}
        onProfileChange={setProfile}
      />
    </>
  );
}

function TwoLandmarkEditorFlowHarness() {
  const [profile, setProfile] = useState(createProfileWithTwoLandmarks());
  const [target, setTarget] = useState<RpgCreationEditorTarget | null>({
    kind: 'landmark',
    mode: 'edit',
    id: 'landmark-1',
  });

  return (
    <RpgCreationEntityEditorModal
      profile={profile}
      target={target}
      onClose={() => setTarget(null)}
      onProfileChange={setProfile}
    />
  );
}

function LandmarkEditorNoNpcFlowHarness() {
  const [profile, setProfile] = useState<CustomWorldProfile>(() => ({
    ...createProfileWithLandmark(),
    storyNpcs: [],
    landmarks: [
      {
        id: 'landmark-1',
        name: '空港栈桥',
        description: '暂时没有角色驻留的场景。',
        imageSrc: '/generated-custom-world-scenes/empty-scene.png',
        sceneNpcIds: [],
        connections: [],
      },
    ],
    sceneChapterBlueprints: [
      {
        ...createSceneChapter(
          'landmark-1',
          '空港栈桥',
          '/generated-custom-world-scenes/empty',
        ),
        acts: [
          {
            ...createSceneAct(
              'landmark-1',
              0,
              '/generated-custom-world-scenes/empty-act-1.png',
            ),
            encounterNpcIds: [],
            primaryNpcId: '',
            oppositeNpcId: '',
          },
          {
            ...createSceneAct(
              'landmark-1',
              1,
              '/generated-custom-world-scenes/empty-act-2.png',
            ),
            encounterNpcIds: [],
            primaryNpcId: '',
            oppositeNpcId: '',
          },
        ],
      },
    ],
  }));
  const [target, setTarget] = useState<RpgCreationEditorTarget | null>({
    kind: 'landmark',
    mode: 'edit',
    id: 'landmark-1',
  });

  return (
    <RpgCreationEntityEditorModal
      profile={profile}
      target={target}
      onClose={() => setTarget(null)}
      onProfileChange={setProfile}
    />
  );
}

function readLandmarkHarnessProfile() {
  const content = screen.getByTestId('landmark-profile-json').textContent;
  return JSON.parse(content || '{}') as CustomWorldProfile;
}

function getSceneActCard(index: number) {
  const card = screen.getAllByTestId('scene-act-card')[index];
  if (!card) {
    throw new Error(`未找到第 ${index + 1} 个幕卡片`);
  }
  return card;
}

function CampEditorFlowHarness() {
  const [profile, setProfile] = useState<CustomWorldProfile>({
    ...createProfileWithLandmark(),
    camp: {
      id: 'custom-scene-camp',
      name: '潮灯居',
      description: '玩家最初落脚的旧灯塔内院。',
      imageSrc: '/generated-custom-world-scenes/original-camp.png',
      sceneNpcIds: ['story-1', 'story-2', 'story-3'],
      connections: [
        {
          targetLandmarkId: 'landmark-1',
          relativePosition: 'north',
          summary: '北侧通往沉钟栈桥。',
        },
      ],
    },
  });
  const [target, setTarget] = useState<RpgCreationEditorTarget | null>({
    kind: 'camp',
  });

  return (
    <>
      <pre data-testid="camp-profile-json" className="hidden">
        {JSON.stringify(profile)}
      </pre>
      <CustomWorldEntityCatalog
        profile={profile}
        previewCharacters={[]}
        activeTab="landmarks"
        onActiveTabChange={() => {}}
        onEditTarget={setTarget}
        onProfileChange={setProfile}
        onDeleteStoryNpcs={() => {}}
        onDeleteLandmarks={() => {}}
      />
      <RpgCreationEntityEditorModal
        profile={profile}
        target={target}
        onClose={() => setTarget(null)}
        onProfileChange={setProfile}
      />
    </>
  );
}

function CoverEditorFlowHarness() {
  const [profile, setProfile] = useState<CustomWorldProfile>({
    ...createProfileWithLandmark(),
    cover: {
      sourceType: 'default',
      imageSrc: null,
      characterRoleIds: ['playable-1'],
    },
  });
  const [target, setTarget] = useState<RpgCreationEditorTarget | null>({
    kind: 'cover',
  });

  return (
    <>
      <pre data-testid="cover-profile-json" className="hidden">
        {JSON.stringify(profile)}
      </pre>
      <RpgCreationEntityEditorModal
        profile={profile}
        target={target}
        onClose={() => setTarget(null)}
        onProfileChange={setProfile}
      />
    </>
  );
}

function readCoverHarnessProfile() {
  const content = screen.getByTestId('cover-profile-json').textContent;
  return JSON.parse(content || '{}') as CustomWorldProfile;
}

function findNearestClassName(element: HTMLElement, className: string) {
  let current: HTMLElement | null = element;

  while (current) {
    if (current.className.includes(className)) {
      return current.className;
    }
    current = current.parentElement;
  }

  return '';
}

function readCampHarnessProfile() {
  const content = screen.getByTestId('camp-profile-json').textContent;
  return JSON.parse(content || '{}') as CustomWorldProfile;
}

test('playable角色打开AI工坊后不会自动关闭', async () => {
  const user = userEvent.setup();
  const handleClose = vi.fn();

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'playable', mode: 'edit', id: 'playable-1' }}
      onClose={handleClose}
      onProfileChange={vi.fn()}
    />,
  );

  await user.click(screen.getByRole('button', { name: 'AI生成' }));

  await waitFor(() => {
    expect(screen.getByText('AI角色形象生成')).toBeTruthy();
  });

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

test('可扮演角色技能动作状态复用暗色平台胶囊标签', () => {
  const profile = createProfile();
  profile.playableNpcs = [
    {
      ...profile.playableNpcs[0]!,
      imageSrc: '/generated-custom-world-roles/playable-1.png',
      generatedVisualAssetId: 'visual-playable-1',
      generatedAnimationSetId: 'animation-playable-1',
      initialItems: [
        {
          id: 'starter-token',
          name: '起始信物',
          category: '道具',
          quantity: 1,
          rarity: 'rare',
          description: '角色开局携带的测试道具。',
          tags: ['开局', '信物'],
        },
      ],
      skills: [
        {
          id: 'tide-slash',
          name: '潮刃突进',
          summary: '向前突进并斩击。',
          style: 'burst',
        },
      ],
    },
  ];

  render(
    <RpgCreationEntityEditorModal
      profile={profile}
      target={{ kind: 'playable', mode: 'edit', id: 'playable-1' }}
      onClose={vi.fn()}
      onProfileChange={vi.fn()}
    />,
  );

  const actionStatusBadge = screen.getByText('待生成动作');
  const visualStatusBadge = screen.getByText('已应用主图');
  const animationStatusBadge = screen.getByText('已应用动作');
  const initialItemTagBadge = screen.getByText('开局');
  const rolePreviewFrame = screen
    .getByRole('img', { name: '沈砺' })
    .closest('.platform-media-frame');
  const skillPreviewFrame = screen
    .getByRole('img', { name: '潮刃突进' })
    .closest('.platform-media-frame');
  const sectionPanels = ['背景故事', '与其他角色的关系', '技能', '物品'].map(
    (title) => screen.getByText(title).closest('section'),
  );

  expect(actionStatusBadge.className).toContain('rounded-full');
  expect(actionStatusBadge.className).toContain('font-black');
  expect(actionStatusBadge.className).toContain('bg-black/20');
  expect(actionStatusBadge.className).toContain('text-zinc-400');
  expect(visualStatusBadge.className).toContain('rounded-full');
  expect(visualStatusBadge.className).toContain('font-black');
  expect(visualStatusBadge.className).toContain('bg-emerald-500/10');
  expect(animationStatusBadge.className).toContain('rounded-full');
  expect(animationStatusBadge.className).toContain('font-black');
  expect(animationStatusBadge.className).toContain('bg-amber-500/10');
  expect(initialItemTagBadge.className).toContain('rounded-full');
  expect(initialItemTagBadge.className).toContain('font-black');
  expect(initialItemTagBadge.className).toContain('bg-black/20');
  expect(rolePreviewFrame?.className).toContain('platform-media-frame');
  expect(rolePreviewFrame?.className).toContain('aspect-square');
  expect(skillPreviewFrame?.className).toContain('platform-media-frame');
  expect(skillPreviewFrame?.className).toContain('rounded-none');
  for (const sectionPanel of sectionPanels) {
    expect(sectionPanel?.className).toContain('border-white/10');
    expect(sectionPanel?.className).toContain('bg-black/25');
    expect(sectionPanel?.className).toContain('rounded-[1.25rem]');
  }
});

test('可扮演角色空态复用暗色平台空态', () => {
  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'playable', mode: 'edit', id: 'playable-1' }}
      onClose={vi.fn()}
      onProfileChange={vi.fn()}
    />,
  );

  for (const label of [
    '还没有配置与其他角色的关系。',
    '还没有配置角色技能。',
    '还没有配置角色物品。',
  ]) {
    const emptyState = screen.getByText(label);
    expect(emptyState.className).toContain('platform-empty-state');
    expect(emptyState.className).toContain('border-dashed');
    expect(emptyState.className).toContain('bg-black/20');
  }
});

test('场景角色打开AI工坊后不会自动关闭', async () => {
  const user = userEvent.setup();
  const handleClose = vi.fn();

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'story', mode: 'edit', id: 'story-1' }}
      onClose={handleClose}
      onProfileChange={vi.fn()}
    />,
  );

  await user.click(screen.getByRole('button', { name: 'AI生成' }));

  await waitFor(() => {
    expect(screen.getByText('AI角色形象生成')).toBeTruthy();
  });

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

test('可扮演角色未修改时右上角关闭不会弹确认', async () => {
  const user = userEvent.setup();
  const handleClose = vi.fn();

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'playable', mode: 'edit', id: 'playable-1' }}
      onClose={handleClose}
      onProfileChange={vi.fn()}
    />,
  );

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

  expect(handleClose).toHaveBeenCalledTimes(1);
  expect(screen.queryByText('确认关闭')).toBeNull();
});

test('可扮演角色修改后右上角关闭才弹确认', async () => {
  const user = userEvent.setup();
  const handleClose = vi.fn();

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'playable', mode: 'edit', id: 'playable-1' }}
      onClose={handleClose}
      onProfileChange={vi.fn()}
    />,
  );

  const nameInput = screen.getByDisplayValue('沈砺');
  await user.clear(nameInput);
  await user.type(nameInput, '沈砺·改');
  await user.click(screen.getByRole('button', { name: '关闭' }));

  expect(handleClose).not.toHaveBeenCalled();
  const dialog = screen.getByRole('dialog', { name: '确认关闭' });
  expect(
    within(dialog).getByText('当前修改尚未保存，确认关闭吗？'),
  ).toBeTruthy();

  await user.click(within(dialog).getByRole('button', { name: '继续编辑' }));
  expect(handleClose).not.toHaveBeenCalled();
  expect(screen.queryByRole('dialog', { name: '确认关闭' })).toBeNull();

  await user.click(screen.getByRole('button', { name: '关闭' }));
  await user.click(screen.getByRole('button', { name: '确认关闭' }));

  expect(handleClose).toHaveBeenCalledTimes(1);
});

test('可扮演角色至少保留一个背景章节时使用统一提示弹窗', async () => {
  const user = userEvent.setup();
  const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'playable', mode: 'edit', id: 'playable-1' }}
      onClose={vi.fn()}
      onProfileChange={vi.fn()}
    />,
  );

  const deleteChapterButtons = () =>
    screen.getAllByRole('button', { name: '删除章节' });

  await user.click(deleteChapterButtons()[0]!);
  await user.click(deleteChapterButtons()[0]!);
  await user.click(deleteChapterButtons()[0]!);
  expect(deleteChapterButtons()).toHaveLength(1);

  await user.click(deleteChapterButtons()[0]!);

  const dialog = await screen.findByRole('dialog', { name: '提示' });
  const actionButton = within(dialog).getByRole('button', { name: '知道了' });

  expect(within(dialog).getByText('至少保留一个背景章节。')).toBeTruthy();
  expect(dialog.querySelector('.platform-icon-badge')).toBeTruthy();
  expect(actionButton.className).toContain('platform-button');
  expect(alertSpy).not.toHaveBeenCalled();
  expect(screen.getByText('编辑角色：沈砺')).toBeTruthy();

  await user.click(actionButton);
  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '提示' })).toBeNull();
  });

  alertSpy.mockRestore();
});

test('场景角色未修改时右上角关闭不会弹确认', async () => {
  const user = userEvent.setup();
  const handleClose = vi.fn();

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'story', mode: 'edit', id: 'story-1' }}
      onClose={handleClose}
      onProfileChange={vi.fn()}
    />,
  );

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

  expect(handleClose).toHaveBeenCalledTimes(1);
  expect(screen.queryByText('确认关闭')).toBeNull();
});

test('场景角色修改后右上角关闭才弹确认', async () => {
  const user = userEvent.setup();
  const handleClose = vi.fn();

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'story', mode: 'edit', id: 'story-1' }}
      onClose={handleClose}
      onProfileChange={vi.fn()}
    />,
  );

  const nameInput = screen.getByDisplayValue('顾潮音');
  await user.clear(nameInput);
  await user.type(nameInput, '顾潮音·改');
  await user.click(screen.getByRole('button', { name: '关闭' }));

  expect(handleClose).not.toHaveBeenCalled();
  const dialog = screen.getByRole('dialog', { name: '确认关闭' });
  expect(
    within(dialog).getByText('当前修改尚未保存，确认关闭吗？'),
  ).toBeTruthy();

  await user.click(within(dialog).getByRole('button', { name: '继续编辑' }));
  expect(handleClose).not.toHaveBeenCalled();
  expect(screen.queryByRole('dialog', { name: '确认关闭' })).toBeNull();

  await user.click(screen.getByRole('button', { name: '关闭' }));
  await user.click(screen.getByRole('button', { name: '确认关闭' }));

  expect(handleClose).toHaveBeenCalledTimes(1);
});

test('世界页基本设定编辑按钮打开基本设定编辑目标', async () => {
  const user = userEvent.setup();
  const handleEditTarget = vi.fn();

  render(
    <CustomWorldEntityCatalog
      profile={createProfile()}
      previewCharacters={[]}
      activeTab="world"
      onActiveTabChange={() => {}}
      onEditTarget={handleEditTarget}
      onProfileChange={() => {}}
    />,
  );

  const editButtons = screen.getAllByRole('button', { name: '编辑' });
  const foundationEditButton = editButtons[1];
  expect(foundationEditButton).toBeDefined();
  await user.click(foundationEditButton as HTMLElement);

  expect(handleEditTarget).toHaveBeenCalledWith({ kind: 'foundation' });
});

test('实体目录 sticky rail 改用共享 segmented tabs 并保留计数标签', async () => {
  const user = userEvent.setup();
  const handleActiveTabChange = vi.fn();

  render(
    <CustomWorldEntityCatalog
      profile={createProfile()}
      previewCharacters={[]}
      activeTab="world"
      onActiveTabChange={handleActiveTabChange}
      onEditTarget={() => {}}
      onProfileChange={() => {}}
    />,
  );

  const tablist = screen.getByRole('tablist', { name: '世界实体目录' });
  const worldTab = screen.getByRole('tab', { name: '世界 1' });
  const storyTab = screen.getByRole('tab', { name: '场景角色 1' });

  expect(tablist.className).toContain('flex');
  expect(tablist.className).toContain('overflow-x-auto');
  expect(worldTab.getAttribute('aria-selected')).toBe('true');
  expect(storyTab.getAttribute('aria-selected')).toBe('false');
  expect(within(storyTab).getByText('场景角色')).toBeTruthy();
  expect(within(storyTab).getByText('1')).toBeTruthy();

  await user.click(storyTab);

  expect(handleActiveTabChange).toHaveBeenCalledWith('story');
});

test('基本设定用分号拆分成标签展示', () => {
  const profile = {
    ...createProfile(),
    anchorContent: {
      worldPromise: '机械微生物吞并进化；角色被迫寄生改造；在失控系统里求生',
      playerFantasy: null,
      themeBoundary: null,
      playerEntryPoint: null,
      coreConflict: null,
      keyRelationships: null,
      hiddenLines: null,
      iconicElements: null,
    },
  } as CustomWorldProfile;

  render(
    <CustomWorldEntityCatalog
      profile={profile}
      previewCharacters={[]}
      activeTab="world"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={() => {}}
    />,
  );

  const foundationSection = screen.getByText('世界承诺').closest('div');

  expect(foundationSection).not.toBeNull();
  expect(screen.getByText('机械微生物吞并进化')).toBeTruthy();
  expect(screen.getByText('角色被迫寄生改造')).toBeTruthy();
  expect(screen.getByText('在失控系统里求生')).toBeTruthy();
});

test('基本设定目标打开独立编辑面板', () => {
  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'foundation' }}
      onClose={vi.fn()}
      onProfileChange={vi.fn()}
    />,
  );

  expect(screen.getByText('编辑基本设定')).toBeTruthy();
  expect(screen.queryByText('编辑世界信息')).toBeNull();
});

test('基本设定面板只编辑六个角色维度名称', async () => {
  const user = userEvent.setup();
  const savedProfileRef: { current: CustomWorldProfile | null } = {
    current: null,
  };

  render(
    <RpgCreationEntityEditorModal
      profile={createProfile()}
      target={{ kind: 'foundation' }}
      onClose={() => {}}
      onProfileChange={(profile) => {
        savedProfileRef.current = profile;
      }}
    />,
  );

  expect(screen.getByText('角色维度')).toBeTruthy();
  const nameInputs = screen.getAllByLabelText('维度名称');
  await user.clear(nameInputs[0]!);
  await user.type(nameInputs[0]!, '潮骨');

  expect(screen.queryByLabelText('定义')).toBeNull();
  expect(screen.queryByLabelText('正向信号')).toBeNull();
  expect(screen.queryByLabelText('战斗体现')).toBeNull();

  const saveButton = screen.getByRole('button', { name: /保存修改/u });
  expect(saveButton.className).toContain('platform-action-button--editor-dark');
  expect(saveButton.className).toContain('rounded-full');

  await user.click(saveButton);

  expect(savedProfileRef.current?.attributeSchema.slots[0]?.name).toBe('潮骨');
});

test('可扮演角色列表使用缩略卡片并点击进入编辑', async () => {
  const user = userEvent.setup();
  const handleEditTarget = vi.fn();

  render(
    <CustomWorldEntityCatalog
      profile={createProfile()}
      previewCharacters={[]}
      activeTab="playable"
      onActiveTabChange={() => {}}
      onEditTarget={handleEditTarget}
      onProfileChange={vi.fn()}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

  expect(screen.queryByText(/公开背景/u)).toBeNull();
  const playableCard = screen.getByRole('button', { name: /沈砺/u });
  const playableMediaFrame = Array.from(
    playableCard.querySelectorAll<HTMLElement>('.platform-subpanel'),
  ).find((element) => element.className.includes('h-[4.75rem]'));

  expect(playableCard.tagName).toBe('BUTTON');
  expect(playableCard.className).toContain('platform-subpanel');
  expect(playableCard.className).toContain('rounded-[1.3rem]');
  expect(playableMediaFrame).toBeTruthy();
  expect(playableMediaFrame?.className).toContain('platform-subpanel');
  expect(playableMediaFrame?.className).toContain('rounded-[1rem]');
  expect(playableMediaFrame?.className).toContain('p-0');

  await user.click(playableCard);

  expect(handleEditTarget).toHaveBeenCalledWith({
    kind: 'playable',
    mode: 'edit',
    id: 'playable-1',
  });
});

test('实体目录批量选择卡片和选择徽标复用平台公共组件 chrome', async () => {
  const user = userEvent.setup();

  render(
    <CustomWorldEntityCatalog
      profile={createProfileWithLandmark()}
      previewCharacters={[]}
      activeTab="story"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={() => {}}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

  await user.click(screen.getByRole('button', { name: '批量删除' }));

  const storyCard = screen.getByRole('button', { name: /顾潮音/u });
  const idleBadge = within(storyCard).getByText('选择');

  expect(storyCard.tagName).toBe('BUTTON');
  expect(storyCard.className).toContain('platform-subpanel');
  expect(storyCard.className).toContain('rounded-[1.3rem]');
  expect(idleBadge.className).toContain('bg-[var(--platform-subpanel-fill)]');
  expect(idleBadge.className).toContain('text-[var(--platform-text-soft)]');

  await user.click(storyCard);

  const selectedStoryCard = screen.getByRole('button', { name: /顾潮音/u });
  const selectedBadge = within(selectedStoryCard).getByText('已选');

  expect(selectedStoryCard.className).not.toContain('platform-subpanel');
  expect(selectedStoryCard.className).toContain(
    'border-[var(--platform-button-danger-border)]',
  );
  expect(selectedStoryCard.className).toContain(
    'bg-[var(--platform-button-danger-fill)]',
  );
  expect(selectedBadge.className).toContain(
    'border-[var(--platform-button-danger-border)]',
  );
});

test('实体目录场景图片框复用平台媒体框 chrome', () => {
  render(
    <CustomWorldEntityCatalog
      profile={createProfileWithLandmark()}
      previewCharacters={[]}
      activeTab="landmarks"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={() => {}}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

  const sceneImage = screen.getByRole('img', { name: '沉钟栈桥' });
  const mediaFrame = sceneImage.closest('div.relative');

  expect(mediaFrame?.className).toContain('aspect-[16/9]');
  expect(mediaFrame?.className).toContain(
    'border-[var(--platform-subpanel-border)]',
  );
  expect(mediaFrame?.className).toContain('radial-gradient');
  expect(sceneImage.className).toContain('object-cover');
});

test('实体目录搜索框和空态复用平台公共组件 chrome', async () => {
  const user = userEvent.setup();

  render(
    <CustomWorldEntityCatalog
      profile={createProfile()}
      previewCharacters={[]}
      activeTab="playable"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={() => {}}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

  const searchInput = screen.getByPlaceholderText('搜索角色名称、称号、标签');

  expect(searchInput.className).toContain(
    'border-[var(--platform-subpanel-border)]',
  );
  expect(searchInput.className).toContain('focus:ring-2');

  await user.type(searchInput, '没有这个角色');

  const emptyState = screen
    .getByText('当前没有符合搜索条件的可扮演角色。')
    .closest('div.rounded-2xl');

  expect(emptyState?.className).toContain('border-dashed');
  expect(emptyState?.className).toContain('bg-white/52');
});

test('世界页统计和基本设定复用平台公共组件 chrome', () => {
  render(
    <CustomWorldEntityCatalog
      profile={createProfile()}
      previewCharacters={[]}
      activeTab="world"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={() => {}}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

  const archiveScaleSection = screen
    .getByText('档案规模')
    .closest('.platform-surface');
  const playableStatCard = within(archiveScaleSection as HTMLElement).getByText(
    '可扮演角色',
  ).parentElement;
  const worldTonePanel = screen
    .getByText(/世界基调：/u)
    .closest('div.rounded-2xl');
  const roleDimensionPanel = screen
    .getByText('角色维度')
    .closest('div.rounded-2xl');
  const foundationPanel = screen
    .getByText('玩家幻想')
    .closest('div.rounded-2xl');

  expect(playableStatCard?.className).toContain('platform-subpanel');
  expect(playableStatCard?.className).toContain('bg-white/68');
  expect(playableStatCard?.className).toContain('rounded-xl');
  expect(worldTonePanel?.className).toContain('platform-subpanel');
  expect(roleDimensionPanel?.className).toContain('platform-subpanel');
  expect(foundationPanel?.className).toContain('platform-subpanel');
  expect(screen.getByText('角色维度').className).toContain('tracking-[0.18em]');
});

test('可扮演角色删除改用统一确认弹窗', async () => {
  const user = userEvent.setup();
  const handleProfileChange = vi.fn();

  render(
    <CustomWorldEntityCatalog
      profile={{
        ...createProfile(),
        playableNpcs: [
          createPlayableRole('playable-1', '沈砺'),
          createPlayableRole('playable-2', '闻潮'),
        ],
      }}
      previewCharacters={[]}
      activeTab="playable"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={handleProfileChange}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

  await user.click(screen.getAllByRole('button', { name: '删除' })[0]!);

  const dialog = screen.getByRole('dialog', { name: '删除角色' });
  expect(
    within(dialog).getByText('确认删除可扮演角色「沈砺」吗？'),
  ).toBeTruthy();

  await user.click(within(dialog).getByRole('button', { name: '取消' }));
  expect(handleProfileChange).not.toHaveBeenCalled();

  await user.click(screen.getAllByRole('button', { name: '删除' })[0]!);
  await user.click(
    screen.getByRole('button', {
      name: '确认删除',
    }),
  );

  expect(handleProfileChange).toHaveBeenCalledTimes(1);
  expect(handleProfileChange.mock.calls[0]?.[0].playableNpcs).toHaveLength(1);
  expect(handleProfileChange.mock.calls[0]?.[0].playableNpcs[0]?.name).toBe(
    '闻潮',
  );
});

test('最后一个可扮演角色不可删除时使用平台状态弹窗', async () => {
  const user = userEvent.setup();
  const handleProfileChange = vi.fn();

  render(
    <CustomWorldEntityCatalog
      profile={createProfile()}
      previewCharacters={[]}
      activeTab="playable"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={handleProfileChange}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

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

  const dialog = screen.getByRole('dialog', { name: '无法删除' });
  expect(
    within(dialog).getByText(
      '至少保留一个可扮演角色，才能正常进入自定义世界。',
    ),
  ).toBeTruthy();

  fireEvent.click(dialog.parentElement as HTMLElement);
  expect(screen.getByRole('dialog', { name: '无法删除' })).toBeTruthy();

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

  expect(handleProfileChange).not.toHaveBeenCalled();
  expect(screen.queryByRole('dialog', { name: '无法删除' })).toBeNull();
});

test('批量删除场景角色使用统一确认弹窗', async () => {
  const user = userEvent.setup();
  const handleDeleteStoryNpcs = vi.fn();

  render(
    <CustomWorldEntityCatalog
      profile={createProfileWithLandmark()}
      previewCharacters={[]}
      activeTab="story"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={() => {}}
      onDeleteStoryNpcs={handleDeleteStoryNpcs}
      onDeleteLandmarks={() => {}}
    />,
  );

  await user.click(screen.getByRole('button', { name: '批量删除' }));
  await user.click(screen.getByRole('button', { name: /顾潮音/u }));
  await user.click(screen.getByRole('button', { name: /闻雪汀/u }));
  await user.click(screen.getByRole('button', { name: '删除选中' }));

  const dialog = screen.getByRole('dialog', { name: '批量删除' });
  expect(
    within(dialog).getByText('确认批量删除 2 个场景角色吗？'),
  ).toBeTruthy();
  expect(handleDeleteStoryNpcs).not.toHaveBeenCalled();

  await user.click(within(dialog).getByRole('button', { name: '确认删除' }));

  expect(handleDeleteStoryNpcs).toHaveBeenCalledWith(['story-1', 'story-2']);
  expect(screen.queryByRole('button', { name: '删除选中' })).toBeNull();
});

test('实体目录在空 id 列表项下不会触发重复 key 警告', () => {
  const consoleErrorSpy = vi
    .spyOn(console, 'error')
    .mockImplementation(() => undefined);

  render(
    <CustomWorldEntityCatalog
      profile={{
        ...createProfile(),
        playableNpcs: [
          createPlayableRole('', '沈砺'),
          createPlayableRole('', '闻潮'),
        ],
      }}
      previewCharacters={[]}
      activeTab="playable"
      onActiveTabChange={() => {}}
      onEditTarget={() => {}}
      onProfileChange={vi.fn()}
      onDeleteStoryNpcs={() => {}}
      onDeleteLandmarks={() => {}}
    />,
  );

  expect(screen.getByRole('button', { name: /沈砺/u })).toBeTruthy();
  expect(screen.getByRole('button', { name: /闻潮/u })).toBeTruthy();

  const duplicateKeyCalls = consoleErrorSpy.mock.calls.filter((call) =>
    call.some(
      (arg) =>
        typeof arg === 'string' &&
        arg.includes('Encountered two children with the same key'),
    ),
  );

  expect(duplicateKeyCalls).toHaveLength(0);
});

test('场景图片保存后会同步更新编辑页和场景列表', async () => {
  mockedRpgCreationAssetClient.generateSceneImage.mockClear();
  mockedRpgCreationAssetClient.generateSceneImage.mockResolvedValue({
    imageSrc: '/generated-custom-world-scenes/updated-scene.png',
    assetId: 'asset-1',
    model: 'wan2.2-t2i-flash',
    size: '1280*720',
    taskId: 'task-1',
    prompt: '更新后的场景图',
  });

  const user = userEvent.setup();

  render(<LandmarkEditorFlowHarness />);

  const initialListImage = screen.getByRole('img', { name: '沉钟栈桥' });
  expect(initialListImage.getAttribute('src')).toBe(
    '/generated-custom-world-scenes/original-scene.png',
  );

  const firstActCard = getSceneActCard(0);
  await user.click(
    within(firstActCard).getByRole('button', { name: '配置背景' }),
  );
  await waitFor(() => {
    expect(screen.getByText('配置幕背景：第1幕')).toBeTruthy();
  });
  expect(screen.queryByText('场景图片')).toBeNull();
  expect(screen.queryByText('场景内 NPC')).toBeNull();

  await user.click(screen.getByRole('button', { name: 'AI生成' }));
  await waitFor(() => {
    expect(screen.getByText('智能生成：沉钟栈桥')).toBeTruthy();
  });

  await user.click(screen.getByRole('button', { name: '开始生成' }));
  await waitFor(() => {
    expect(
      mockedRpgCreationAssetClient.generateSceneImage,
    ).toHaveBeenCalledTimes(1);
  });
  await waitFor(() => {
    const generatedStatus = screen.getByText('已生成完毕，请保存后再退出页面');
    expect(generatedStatus.className).toContain('platform-status-message');
    expect(generatedStatus.className).toContain('text-emerald-50');
  });

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

  await waitFor(() => {
    expect(screen.queryByText('智能生成：沉钟栈桥')).toBeNull();
  });

  await waitFor(() => {
    expect(
      screen.getByRole('img', { name: '第1幕背景预览' }).getAttribute('src'),
    ).toBe('/generated-custom-world-scenes/updated-scene.png');
  });

  await user.click(screen.getByRole('button', { name: '保存背景' }));

  await waitFor(() => {
    expect(screen.queryByText('配置幕背景：第1幕')).toBeNull();
  });

  await user.click(screen.getByRole('button', { name: /保存修改/u }));

  await waitFor(() => {
    expect(
      screen.getByRole('img', { name: '沉钟栈桥' }).getAttribute('src'),
    ).toBe('/generated-custom-world-scenes/updated-scene.png');
  });

  const savedProfile = readLandmarkHarnessProfile();
  expect(savedProfile.landmarks[0]?.imageSrc).toBe(
    '/generated-custom-world-scenes/updated-scene.png',
  );
  const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
    (entry) => entry.sceneId === 'landmark-1',
  );
  expect(savedSceneChapter?.acts[0]?.backgroundImageSrc).toBe(
    '/generated-custom-world-scenes/updated-scene.png',
  );
  expect(savedSceneChapter?.acts[1]?.backgroundImageSrc).not.toBe(
    '/generated-custom-world-scenes/updated-scene.png',
  );
  expect(savedSceneChapter?.acts[2]?.backgroundImageSrc).not.toBe(
    '/generated-custom-world-scenes/updated-scene.png',
  );
});

test('幕背景生成未保存时退出使用统一确认弹窗', async () => {
  mockedRpgCreationAssetClient.generateSceneImage.mockClear();
  mockedRpgCreationAssetClient.generateSceneImage.mockResolvedValue({
    imageSrc: '/generated-custom-world-scenes/unsaved-scene.png',
    assetId: 'asset-unsaved-1',
    model: 'wan2.2-t2i-flash',
    size: '1280*720',
    taskId: 'task-unsaved-1',
    prompt: '未保存的场景图',
  });

  const user = userEvent.setup();

  render(<LandmarkEditorFlowHarness />);

  await user.click(
    within(getSceneActCard(0)).getByRole('button', { name: '配置背景' }),
  );
  await waitFor(() => {
    expect(screen.getByText('配置幕背景：第1幕')).toBeTruthy();
  });
  await user.click(screen.getByRole('button', { name: 'AI生成' }));
  await waitFor(() => {
    expect(screen.getByText('智能生成：沉钟栈桥')).toBeTruthy();
  });
  await user.click(screen.getByRole('button', { name: '开始生成' }));
  await waitFor(() => {
    expect(
      mockedRpgCreationAssetClient.generateSceneImage,
    ).toHaveBeenCalledTimes(1);
  });

  const closeButtons = screen.getAllByRole('button', { name: '关闭' });
  await user.click(closeButtons[closeButtons.length - 1]!);

  const exitDialog = screen.getByRole('dialog', { name: '确认退出' });
  expect(
    within(exitDialog).getByText(
      '当前生成画面还未保存，退出后将丢失这次生成结果，仍然退出吗？',
    ),
  ).toBeTruthy();

  await user.click(
    within(exitDialog).getByRole('button', { name: '继续编辑' }),
  );
  expect(screen.getByText('智能生成：沉钟栈桥')).toBeTruthy();

  const reopenedCloseButtons = screen.getAllByRole('button', { name: '关闭' });
  await user.click(reopenedCloseButtons[reopenedCloseButtons.length - 1]!);
  await user.click(screen.getByRole('button', { name: '仍然退出' }));

  await waitFor(() => {
    expect(screen.queryByText('智能生成：沉钟栈桥')).toBeNull();
  });
});

test('开局场景图片保存后会同步更新编辑页和场景列表', async () => {
  mockedRpgCreationAssetClient.generateSceneImage.mockClear();
  mockedRpgCreationAssetClient.generateSceneImage.mockResolvedValue({
    imageSrc: '/generated-custom-world-scenes/updated-camp.png',
    assetId: 'asset-camp-1',
    model: 'wan2.2-t2i-flash',
    size: '1280*720',
    taskId: 'task-camp-1',
    prompt: '更新后的开局场景图',
  });

  const user = userEvent.setup();

  render(<CampEditorFlowHarness />);

  const initialListImage = screen.getByRole('img', { name: '潮灯居' });
  expect(initialListImage.getAttribute('src')).toBe(
    '/generated-custom-world-scenes/original-camp.png',
  );

  const firstActCard = getSceneActCard(0);
  await user.click(
    within(firstActCard).getByRole('button', { name: '配置背景' }),
  );
  await waitFor(() => {
    expect(screen.getByText('配置幕背景：第1幕')).toBeTruthy();
  });
  expect(screen.queryByText('场景图片')).toBeNull();
  expect(screen.queryByText('场景内 NPC')).toBeNull();

  await user.click(screen.getByRole('button', { name: 'AI生成' }));
  await waitFor(() => {
    expect(screen.getByText('智能生成：潮灯居')).toBeTruthy();
  });

  await user.click(screen.getByRole('button', { name: '开始生成' }));
  await waitFor(() => {
    expect(
      mockedRpgCreationAssetClient.generateSceneImage,
    ).toHaveBeenCalledTimes(1);
  });

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

  await waitFor(() => {
    expect(screen.queryByText('智能生成：潮灯居')).toBeNull();
  });

  await waitFor(() => {
    expect(
      screen.getByRole('img', { name: '第1幕背景预览' }).getAttribute('src'),
    ).toBe('/generated-custom-world-scenes/updated-camp.png');
  });

  await user.click(screen.getByRole('button', { name: '保存背景' }));

  await waitFor(() => {
    expect(screen.queryByText('配置幕背景：第1幕')).toBeNull();
  });

  await user.click(screen.getByRole('button', { name: /保存修改/u }));

  await waitFor(() => {
    expect(
      screen.getByRole('img', { name: '潮灯居' }).getAttribute('src'),
    ).toBe('/generated-custom-world-scenes/updated-camp.png');
  });

  const savedProfile = readCampHarnessProfile();
  expect(savedProfile.camp?.imageSrc).toBe(
    '/generated-custom-world-scenes/updated-camp.png',
  );
  const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
    (entry) => entry.sceneId === 'custom-scene-camp',
  );
  expect(savedSceneChapter?.acts[0]?.backgroundImageSrc).toBe(
    '/generated-custom-world-scenes/updated-camp.png',
  );
  expect(savedSceneChapter?.acts[1]?.backgroundImageSrc).not.toBe(
    '/generated-custom-world-scenes/updated-camp.png',
  );
  expect(savedSceneChapter?.acts[2]?.backgroundImageSrc).not.toBe(
    '/generated-custom-world-scenes/updated-camp.png',
  );
});

test('开局场景在场景配置面板中与普通场景使用同级参数并可保存', async () => {
  const user = userEvent.setup();

  render(<CampEditorFlowHarness />);

  expect(screen.getByText('多幕配置')).toBeTruthy();
  expect(screen.getByText('场景连接关系')).toBeTruthy();
  expect(screen.queryByText('场景图片')).toBeNull();
  expect(screen.queryByText('场景内 NPC')).toBeNull();
  expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3);

  const firstActCard = getSceneActCard(0);
  await user.click(
    within(firstActCard).getAllByTestId('scene-act-slot-button')[0]!,
  );
  await waitFor(() => {
    expect(screen.getByText('配置角色：第1幕 · 主角色槽位')).toBeTruthy();
  });

  await user.click(screen.getByRole('button', { name: /闻雪汀/u }));
  await user.click(screen.getByRole('button', { name: '保存角色' }));

  await waitFor(() => {
    expect(screen.queryByText('配置角色：第1幕 · 主角色槽位')).toBeNull();
  });

  await user.click(screen.getByRole('button', { name: /保存修改/u }));

  await waitFor(() => {
    expect(screen.queryByText('编辑场景：潮灯居')).toBeNull();
  });

  const savedProfile = readCampHarnessProfile();
  const openingSceneChapter = savedProfile.sceneChapterBlueprints?.find(
    (entry) => entry.sceneId === 'custom-scene-camp',
  );

  expect(savedProfile.camp?.sceneNpcIds).toContain('story-2');
  expect(savedProfile.camp?.connections).toEqual([
    {
      targetLandmarkId: 'landmark-1',
      relativePosition: 'north',
      summary: '北侧通往沉钟栈桥。',
    },
  ]);
  expect(openingSceneChapter).toBeTruthy();
  expect(openingSceneChapter?.acts[0]?.encounterNpcIds[0]).toBe('story-2');
  expect(openingSceneChapter?.acts[1]?.encounterNpcIds[0]).not.toBe('story-2');
  expect(openingSceneChapter?.acts[1]?.primaryNpcId).not.toBe('story-2');
  expect(openingSceneChapter?.acts[2]?.encounterNpcIds[0]).not.toBe('story-2');
  expect(openingSceneChapter?.acts[2]?.primaryNpcId).not.toBe('story-2');
  expect(openingSceneChapter?.linkedLandmarkIds).toContain('custom-scene-camp');
});

test('开局场景列表与详情幕预览复用同一套幕级图片', async () => {
  const profile = createProfileWithSceneChapters();
  profile.sceneChapterBlueprints![0]!.acts[1]!.backgroundPromptText =
    '第二幕专属背景提示';
  const user = userEvent.setup();

  render(
    <>
      <CustomWorldEntityCatalog
        profile={profile}
        previewCharacters={[]}
        activeTab="landmarks"
        onActiveTabChange={() => {}}
        onEditTarget={() => {}}
        onProfileChange={() => {}}
        onDeleteStoryNpcs={() => {}}
        onDeleteLandmarks={() => {}}
      />
      <RpgCreationEntityEditorModal
        profile={profile}
        target={{ kind: 'camp' }}
        onClose={() => {}}
        onProfileChange={() => {}}
      />
    </>,
  );

  expect(
    screen.getByRole('img', { name: '潮灯居-第2幕' }).getAttribute('src'),
  ).toBe('/generated-custom-world-scenes/camp-act-2.png');
  const campSecondActPreview = screen
    .getByLabelText('潮灯居-第2幕预览')
    .closest('div');
  expect(campSecondActPreview?.className).toContain('platform-subpanel');
  expect(campSecondActPreview?.className).toContain('rounded-xl');
  expect(campSecondActPreview?.className).toContain('p-0');
  expect(
    screen.getByRole('img', { name: '沉钟栈桥-第2幕' }).getAttribute('src'),
  ).toBe('/generated-custom-world-scenes/landmark-act-2.png');
  expect(
    screen.getByRole('img', { name: '第2幕幕背景' }).getAttribute('src'),
  ).toBe('/generated-custom-world-scenes/camp-act-2.png');

  await user.click(
    within(getSceneActCard(1)).getByRole('button', { name: '配置背景' }),
  );

  await waitFor(() => {
    expect(screen.getByText('配置幕背景：第2幕')).toBeTruthy();
  });
  expect(
    screen.getByRole('img', { name: '第2幕背景预览' }).getAttribute('src'),
  ).toBe('/generated-custom-world-scenes/camp-act-2.png');
});

test('开局场景幕背景智能生成复用当前幕图片和幕级提示词', async () => {
  mockedRpgCreationAssetClient.generateSceneImage.mockClear();
  mockedRpgCreationAssetClient.generateSceneImage.mockResolvedValue({
    imageSrc: '/generated-custom-world-scenes/camp-act-2-ai.png',
    assetId: 'asset-camp-act-2',
    model: 'wan2.2-t2i-flash',
    size: '1280*720',
    taskId: 'task-camp-act-2',
    prompt: '第二幕专属背景提示',
  });
  const profile = createProfileWithSceneChapters();
  profile.sceneChapterBlueprints![0]!.acts[1]!.backgroundPromptText =
    '第二幕专属背景提示';
  const user = userEvent.setup();

  render(
    <RpgCreationEntityEditorModal
      profile={profile}
      target={{ kind: 'camp' }}
      onClose={() => {}}
      onProfileChange={() => {}}
    />,
  );

  await user.click(
    within(getSceneActCard(1)).getByRole('button', { name: '配置背景' }),
  );
  await waitFor(() => {
    expect(screen.getByText('配置幕背景：第2幕')).toBeTruthy();
  });

  await user.click(screen.getByRole('button', { name: 'AI生成' }));
  await waitFor(() => {
    expect(screen.getByText('智能生成：潮灯居')).toBeTruthy();
  });

  expect(screen.getByRole('img', { name: '潮灯居' }).getAttribute('src')).toBe(
    '/generated-custom-world-scenes/camp-act-2.png',
  );

  await user.click(screen.getByRole('button', { name: '开始生成' }));
  await waitFor(() => {
    expect(
      mockedRpgCreationAssetClient.generateSceneImage,
    ).toHaveBeenCalledTimes(1);
  });

  const payload =
    mockedRpgCreationAssetClient.generateSceneImage.mock.calls[0]?.[0];
  expect(payload?.userPrompt).toBe('第二幕专属背景提示');
});

test('普通场景世界地图会包含开局场景并高亮当前场景', async () => {
  const user = userEvent.setup();

  render(<LandmarkEditorFlowHarness />);

  await user.click(screen.getByRole('button', { name: '查看世界地图' }));

  await waitFor(() => {
    expect(screen.getByText('世界地图')).toBeTruthy();
  });
  expect(screen.getAllByText('潮灯居').length).toBeGreaterThan(0);
  expect(screen.getAllByText('沉钟栈桥').length).toBeGreaterThan(0);
  const currentBadge = screen.getByText('当前');
  expect(currentBadge.className).toContain('rounded-full');
  expect(currentBadge.className).toContain(
    'bg-[var(--platform-subpanel-fill)]',
  );
});

test('世界地图会展示当前未保存的场景连接', async () => {
  const user = userEvent.setup();

  render(<TwoLandmarkEditorFlowHarness />);

  await user.click(screen.getByText('北'));
  await user.click(screen.getByRole('button', { name: /雾灯塔/u }));
  await waitFor(() => {
    expect(screen.queryByText('北侧连接')).toBeNull();
  });

  await user.click(screen.getByRole('button', { name: '查看世界地图' }));

  await waitFor(() => {
    expect(screen.getByText('世界地图')).toBeTruthy();
  });
  expect(screen.getAllByText('雾灯塔').length).toBeGreaterThan(0);
  expect(screen.getAllByText('北').length).toBeGreaterThan(0);
});

test('场景连接缺少可连接目标时使用统一提示弹窗', async () => {
  const user = userEvent.setup();
  const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});

  render(<LandmarkEditorFlowHarness />);

  await user.click(screen.getByText('北'));

  const dialog = await screen.findByRole('dialog', { name: '提示' });
  const overlay = dialog.parentElement as HTMLElement;

  expect(
    within(dialog).getByText('请先保留至少一个其他场景，才能配置连接关系。'),
  ).toBeTruthy();
  expect(overlay.className).toContain('rpg-editor-notice-dialog-overlay');
  expect(alertSpy).not.toHaveBeenCalled();

  fireEvent.click(overlay);
  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '提示' })).toBeNull();
  });

  alertSpy.mockRestore();
});

test('场景编辑器会在场景内展示槽位化多幕配置并保存', async () => {
  const user = userEvent.setup();

  render(<LandmarkEditorFlowHarness />);

  expect(screen.getByText('多幕配置')).toBeTruthy();
  const connectionPanel = screen.getByText('场景连接关系').closest('section');
  expect(connectionPanel).toBeTruthy();
  expect(connectionPanel?.className).toContain('bg-black/25');
  expect(connectionPanel?.className).toContain('rounded-[1.25rem]');
  expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3);
  expect(screen.queryByText('幕标题')).toBeNull();
  expect(screen.queryByText('幕摘要')).toBeNull();
  expect(screen.queryByText('幕目标')).toBeNull();
  expect(screen.queryByText('过渡铺垫')).toBeNull();

  const firstActCard = getSceneActCard(0);
  expect(
    within(firstActCard).getAllByTestId('scene-act-slot-button'),
  ).toHaveLength(3);

  await user.click(
    within(firstActCard).getByRole('button', { name: '配置背景' }),
  );
  await waitFor(() => {
    expect(screen.getByText('配置幕背景：第1幕')).toBeTruthy();
  });

  const presetImage = screen.getByRole('img', { name: '幕背景预设 1' });
  const presetSrc = presetImage.getAttribute('src');
  const presetFrame = presetImage.closest('.platform-media-frame');
  const presetPanel = screen.getByText('预设背景').closest('section');
  const presetButton = presetImage.closest('button');
  expect(presetPanel?.className).toContain('bg-black/25');
  expect(presetPanel?.className).toContain('rounded-[1.25rem]');
  expect(presetFrame?.className).toContain('aspect-[16/9]');
  expect(presetFrame?.className).toContain('rounded-none');
  expect(presetButton).toBeTruthy();
  if (!presetButton) {
    throw new Error('未找到幕背景预设按钮');
  }
  await user.click(presetButton);
  await user.click(screen.getByRole('button', { name: '保存背景' }));

  await waitFor(() => {
    expect(screen.queryByText('配置幕背景：第1幕')).toBeNull();
  });

  await user.click(
    within(getSceneActCard(0)).getAllByTestId('scene-act-slot-button')[0]!,
  );
  await waitFor(() => {
    expect(screen.getByText('配置角色：第1幕 · 主角色槽位')).toBeTruthy();
  });

  await user.click(screen.getByRole('button', { name: /闻雪汀/u }));
  await user.click(screen.getByRole('button', { name: '保存角色' }));

  await waitFor(() => {
    expect(screen.queryByText('配置角色：第1幕 · 主角色槽位')).toBeNull();
  });

  expect(
    within(getSceneActCard(0)).getByRole('button', {
      name: '配置第1个角色：闻雪汀',
    }),
  ).toBeTruthy();

  await user.click(screen.getByRole('button', { name: /保存修改/u }));

  await waitFor(() => {
    expect(screen.queryByText('编辑场景：沉钟栈桥')).toBeNull();
  });

  const savedProfile = readLandmarkHarnessProfile();
  const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
    (entry) => entry.sceneId === 'landmark-1',
  );

  expect(savedSceneChapter).toBeTruthy();
  expect(savedSceneChapter?.acts[0]?.backgroundImageSrc).toBe(presetSrc);
  expect(savedSceneChapter?.acts[0]?.encounterNpcIds[0]).toBe('story-2');
  expect(savedSceneChapter?.acts[0]?.primaryNpcId).toBe('story-2');
});

test('场景多幕支持新增删除和调序', async () => {
  const user = userEvent.setup();

  render(<LandmarkEditorFlowHarness />);

  await user.click(screen.getByRole('button', { name: '新增一幕' }));
  expect(screen.getAllByTestId('scene-act-card')).toHaveLength(4);

  const secondActCard = getSceneActCard(1);
  await user.click(
    within(secondActCard).getAllByTestId('scene-act-slot-button')[0]!,
  );
  await waitFor(() => {
    expect(screen.getByText('配置角色：第2幕 · 主角色槽位')).toBeTruthy();
  });
  await user.click(screen.getByRole('button', { name: /谢孤灯/u }));
  await user.click(screen.getByRole('button', { name: '保存角色' }));
  await user.click(within(secondActCard).getByRole('button', { name: '下移' }));

  const fourthActCard = getSceneActCard(3);
  await user.click(within(fourthActCard).getByRole('button', { name: '删除' }));
  expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3);

  await user.click(screen.getByRole('button', { name: /保存修改/u }));

  await waitFor(() => {
    expect(screen.queryByText('编辑场景：沉钟栈桥')).toBeNull();
  });

  const savedProfile = readLandmarkHarnessProfile();
  const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
    (entry) => entry.sceneId === 'landmark-1',
  );

  expect(savedSceneChapter?.acts).toHaveLength(3);
  expect(savedSceneChapter?.acts[2]?.primaryNpcId).toBe('story-3');
});

test('每幕角色槽位可以从当前世界所有 NPC 中选择', async () => {
  const user = userEvent.setup();

  render(<LandmarkEditorFlowHarness />);

  await user.click(
    within(getSceneActCard(0)).getAllByTestId('scene-act-slot-button')[0]!,
  );
  await waitFor(() => {
    expect(screen.getByText('配置角色：第1幕 · 主角色槽位')).toBeTruthy();
  });

  const npcOption = screen.getByRole('button', { name: /陆听潮/u });
  const selectBadge = within(npcOption).getByText('选择');

  expect(npcOption).toBeTruthy();
  expect(selectBadge.className).toContain('rounded-full');
  expect(selectBadge.className).toContain('font-black');
  expect(selectBadge.className).toContain('bg-black/20');
  expect(selectBadge.className).toContain('text-zinc-400');

  await user.click(npcOption);
  await user.click(screen.getByRole('button', { name: '保存角色' }));

  await waitFor(() => {
    expect(screen.queryByText('配置角色：第1幕 · 主角色槽位')).toBeNull();
  });

  await user.click(screen.getByRole('button', { name: /保存修改/u }));

  await waitFor(() => {
    expect(screen.queryByText('编辑场景：沉钟栈桥')).toBeNull();
  });

  const savedProfile = readLandmarkHarnessProfile();
  const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
    (entry) => entry.sceneId === 'landmark-1',
  );

  expect(savedSceneChapter?.acts[0]?.encounterNpcIds[0]).toBe('story-4');
  expect(savedProfile.landmarks[0]?.sceneNpcIds).toContain('story-4');
});

test('作品封面来源状态复用暗色平台胶囊标签', () => {
  render(<CoverEditorFlowHarness />);

  const sourceBadge = screen.getByText('当前为默认封面');

  expect(sourceBadge.className).toContain('rounded-full');
  expect(sourceBadge.className).toContain('font-black');
  expect(sourceBadge.className).toContain('bg-black/20');
});

test('场景保存缺少主角色时使用统一提示弹窗', async () => {
  const user = userEvent.setup();
  const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});

  render(<LandmarkEditorNoNpcFlowHarness />);

  await user.click(screen.getByRole('button', { name: /保存修改/u }));

  const dialog = await screen.findByRole('dialog', { name: '提示' });
  expect(within(dialog).getByText('请至少为一幕配置主角色。')).toBeTruthy();
  expect(alertSpy).not.toHaveBeenCalled();
  expect(screen.getByText('编辑场景：空港栈桥')).toBeTruthy();

  fireEvent.keyDown(window, { key: 'Escape' });
  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '提示' })).toBeNull();
  });

  alertSpy.mockRestore();
});

test('场景幕预览会打开当前幕运行时面板', async () => {
  const user = userEvent.setup();

  render(<LandmarkEditorFlowHarness />);

  await user.click(
    within(getSceneActCard(0)).getByRole('button', { name: '幕预览' }),
  );

  await waitFor(() => {
    expect(screen.getByText('幕预览运行时')).toBeTruthy();
  });

  expect(screen.getAllByText('沉钟栈桥').length).toBeGreaterThan(0);

  expect(screen.getByText('隐藏等级徽标')).toBeTruthy();
  expect(screen.getByText('已选择预览角色')).toBeTruthy();
  expect(screen.getByText('runtime-scene-act-preview')).toBeTruthy();
  expect(screen.getByText('landmark-1')).toBeTruthy();
  expect(screen.getByText('play')).toBeTruthy();
  expect(screen.getByText('预览禁用持久化')).toBeTruthy();
  expect(screen.getByText(/顾潮音已经在沉钟栈桥等你/u)).toBeTruthy();
  expect(screen.queryByText('正在载入这一幕的游戏流程...')).toBeNull();

  await user.click(screen.getByRole('button', { name: '结束预览' }));

  await waitFor(() => {
    expect(screen.queryByText('幕预览运行时')).toBeNull();
  });
});

test('作品封面上传会先进入 16:9 裁剪面板再提交到后端', async () => {
  const uploadedCover = {
    imageSrc: '/generated-custom-world-covers/world-1/uploaded/cover.webp',
    assetId: 'custom-cover-upload-1',
    sourceType: 'uploaded' as const,
  };
  let resolveUpload!: (value: typeof uploadedCover) => void;
  const uploadPromise = new Promise<typeof uploadedCover>((resolve) => {
    resolveUpload = resolve;
  });
  const uploadMock = vi
    .mocked(customWorldCoverAssetService.uploadCustomWorldCoverImage)
    .mockReturnValue(uploadPromise);

  class MockFileReader {
    result: string | null = null;
    error: Error | null = null;
    onload: null | (() => void) = null;
    onerror: null | (() => void) = null;

    readAsDataURL() {
      this.result =
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+7aQAAAAASUVORK5CYII=';
      this.onload?.();
    }
  }

  class MockImage {
    onload: null | (() => void) = null;
    onerror: null | (() => void) = null;
    naturalWidth = 1920;
    naturalHeight = 1080;

    set src(_value: string) {
      this.onload?.();
    }
  }

  vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
  vi.stubGlobal('Image', MockImage as unknown as typeof Image);

  const user = userEvent.setup();
  render(<CoverEditorFlowHarness />);

  const input = document.querySelector(
    'input[type="file"]',
  ) as HTMLInputElement | null;
  expect(input).toBeTruthy();
  if (!input) {
    throw new Error('未找到封面上传输入框');
  }

  const file = new File(['cover'], 'cover.png', { type: 'image/png' });
  await user.upload(input, file);

  await waitFor(() => {
    expect(screen.getByText('裁剪上传封面')).toBeTruthy();
  });
  const cropPreviewFrame = screen
    .getByRole('img', { name: '上传封面裁剪预览' })
    .closest('.platform-media-frame');
  const cropResultFrame = screen
    .getByRole('img', { name: '上传封面裁剪结果预览' })
    .closest('.platform-media-frame');
  expect(cropPreviewFrame?.className).toContain('bg-black/40');
  expect(cropResultFrame?.className).toContain('aspect-[16/9]');
  expect(
    screen.getByRole('button', { name: '拖拽右下角裁剪边界' }),
  ).toBeTruthy();
  expect(screen.queryByText('左右位置')).toBeNull();
  expect(screen.queryByText('上下位置')).toBeNull();

  await user.click(screen.getByRole('button', { name: '确认裁剪并上传' }));

  const uploadSavingPanelClassName = findNearestClassName(
    await screen.findByText('正在保存封面资源，请稍候。'),
    'bg-sky-500/8',
  );

  expect(uploadSavingPanelClassName).toContain('border-sky-400/18');
  expect(uploadSavingPanelClassName).toContain('rounded-[1rem]');

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

  const uploadPayload = uploadMock.mock.calls[0]?.[0];
  expect(uploadPayload?.worldName).toBe('潮雾群岛');
  expect(uploadPayload?.cropRect.width).toBeGreaterThan(0);
  expect(uploadPayload?.cropRect.height).toBeGreaterThan(0);

  resolveUpload(uploadedCover);

  await waitFor(() => {
    expect(screen.queryByText('裁剪上传封面')).toBeNull();
  });

  await user.click(screen.getByRole('button', { name: /保存/u }));

  await waitFor(() => {
    expect(screen.queryByText('编辑作品封面')).toBeNull();
  });

  const savedProfile = readCoverHarnessProfile();
  expect(savedProfile.cover?.sourceType).toBe('uploaded');
  expect(savedProfile.cover?.imageSrc).toBe(
    '/generated-custom-world-covers/world-1/uploaded/cover.webp',
  );
});
