// @vitest-environment jsdom

import {
  fireEvent,
  render,
  screen,
  waitFor,
  within,
} from '@testing-library/react';
import { afterEach, describe, expect, test, vi } from 'vitest';

import type { Match3DWorkProfile } from '../../../packages/shared/src/contracts/match3dWorks';
import * as match3dWorksService from '../../services/match3d-works';
import { clearMatch3DGeneratedModelBytesCache } from '../../services/match3dGeneratedModelCache';
import { Match3DResultView } from './Match3DResultView';

const match3dSpritesheetParser = vi.hoisted(() => ({
  loadMatch3DSpritesheetAssetRegions: vi.fn(),
}));

vi.mock('../ResolvedAssetImage', () => ({
  ResolvedAssetImage: ({
    src,
    alt,
    className,
  }: {
    src?: string | null;
    alt?: string;
    className?: string;
  }) => (src ? <img src={src} alt={alt} className={className} /> : null),
}));

vi.mock('../../services/assetReadUrlService', () => ({
  isGeneratedLegacyPath: (value: string) =>
    /^\/?generated-[^/?#]+\/.+/u.test(value.trim()),
  resolveAssetReadUrl: vi.fn((value: string) =>
    Promise.resolve(`https://signed.example.com/${value.replace(/^\/+/u, '')}`),
  ),
}));

vi.mock('../../services/match3d-works', () => ({
  generateMatch3DBackgroundImage: vi.fn(),
  generateMatch3DContainerImage: vi.fn(),
  generateMatch3DCoverImage: vi.fn(),
  generateMatch3DItemAssets: vi.fn(),
  generateMatch3DWorkTags: vi.fn(),
  publishMatch3DWork: vi.fn(),
  updateMatch3DGeneratedItemAssets: vi.fn(),
  updateMatch3DWork: vi.fn(),
}));

vi.mock('../../services/match3dSpritesheetParser', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('../../services/match3dSpritesheetParser')>();
  return {
    ...actual,
    loadMatch3DSpritesheetAssetRegions:
      match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
  };
});

afterEach(() => {
  clearMatch3DGeneratedModelBytesCache();
  vi.clearAllMocks();
  match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockReset();
  vi.unstubAllGlobals();
});

function createDeferred<T>() {
  let resolve!: (value: T) => void;
  let reject!: (reason?: unknown) => void;
  const promise = new Promise<T>((promiseResolve, promiseReject) => {
    resolve = promiseResolve;
    reject = promiseReject;
  });
  return { promise, reject, resolve };
}

function stubMatch3DCoverUpload(dataUrl: string) {
  class MockFileReader {
    result: string | ArrayBuffer | null = dataUrl;
    onload: null | (() => void) = null;
    onerror: null | (() => void) = null;

    readAsDataURL() {
      this.onload?.();
    }
  }

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

function confirmPointCost() {
  const dialogs = screen.getAllByRole('dialog', { name: '确认消耗泥点' });
  const dialog = dialogs[dialogs.length - 1]!;
  fireEvent.click(
    dialog.querySelector('button:last-of-type') as HTMLButtonElement,
  );
}

function createProfile(
  overrides: Partial<Match3DWorkProfile> = {},
): Match3DWorkProfile {
  return {
    workId: 'match3d-work-1',
    profileId: 'match3d-profile-1',
    ownerUserId: 'user-1',
    sourceSessionId: 'match3d-session-1',
    gameName: '水果抓大鹅',
    themeText: '水果',
    summary: '',
    tags: ['水果', '抓大鹅', '经典消除'],
    coverImageSrc: null,
    referenceImageSrc: null,
    clearCount: 12,
    difficulty: 4,
    publicationStatus: 'draft',
    playCount: 0,
    updatedAt: '2026-05-01T00:00:00.000Z',
    publishedAt: null,
    publishReady: false,
    ...overrides,
  };
}

function createReadyGeneratedItemAsset(index: number) {
  return {
    itemId: `match3d-item-${index}`,
    itemName: `物品${index}`,
    imageSrc: `/generated-match3d-assets/session/profile/items/item-${index}/image.png`,
    imageObjectKey: `generated-match3d-assets/session/profile/items/item-${index}/image.png`,
    imageViews: [1, 2, 3, 4, 5].map((viewIndex) => ({
      viewId: `view-${String(viewIndex).padStart(2, '0')}`,
      viewIndex,
      imageSrc: `/generated-match3d-assets/session/profile/items/item-${index}/views/view-${String(viewIndex).padStart(2, '0')}.png`,
      imageObjectKey: `generated-match3d-assets/session/profile/items/item-${index}/views/view-${String(viewIndex).padStart(2, '0')}.png`,
    })),
    modelSrc: null,
    modelObjectKey: null,
    modelFileName: null,
    taskUuid: `task-${index}`,
    subscriptionKey: `sub-${index}`,
    status: 'image_ready' as const,
    error: null,
  };
}

describe('Match3DResultView', () => {
  test('作品信息 Tab 字段命名对齐拼图草稿且描述可为空', async () => {
    const profile = createProfile();
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: profile,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    expect(screen.getByLabelText('作品名称')).toHaveProperty(
      'value',
      '水果抓大鹅',
    );
    expect(screen.getByLabelText('作品描述')).toHaveProperty('value', '');
    expect(screen.getByText('作品标签')).toBeTruthy();
    expect(screen.getByText('水果')).toBeTruthy();
    expect(screen.getByText('抓大鹅')).toBeTruthy();
    expect(screen.queryByRole('button', { name: '封面图' })).toBeNull();

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

    await waitFor(() => {
      expect(match3dWorksService.updateMatch3DWork).toHaveBeenCalledWith(
        'match3d-profile-1',
        expect.objectContaining({
          gameName: '水果抓大鹅',
          summary: '',
          tags: ['水果', '抓大鹅', '经典消除'],
        }),
      );
    });
  });

  test('作品标签支持 AI 生成并写回标签编辑区', async () => {
    vi.mocked(match3dWorksService.generateMatch3DWorkTags).mockResolvedValue({
      tags: ['果园', '抓大鹅', '经典消除', '轻量休闲'],
    });

    render(
      <Match3DResultView
        profile={createProfile({ tags: [] })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' }));

    await waitFor(() => {
      expect(match3dWorksService.generateMatch3DWorkTags).toHaveBeenCalledWith({
        gameName: '水果抓大鹅',
        themeText: '水果',
        summary: '',
      });
      expect(screen.getByText('果园')).toBeTruthy();
      expect(screen.getByText('轻量休闲')).toBeTruthy();
    });
  });

  test('发布面板内支持引用物品素材作为多参考图生成封面', async () => {
    const profile = createProfile({
      generatedItemAssets: [
        {
          itemId: 'match3d-item-1',
          itemName: '草莓',
          imageSrc:
            '/generated-match3d-assets/session/profile/items/i1/image.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/items/i1/image.png',
          modelSrc: null,
          modelObjectKey: null,
          modelFileName: null,
          taskUuid: null,
          subscriptionKey: null,
          status: 'image_ready',
          error: null,
        },
      ],
    });
    const nextProfile = createProfile({
      ...profile,
      coverImageSrc:
        '/generated-match3d-assets/session/profile/cover/task/cover.png',
    });
    const onSaved = vi.fn();
    vi.mocked(match3dWorksService.generateMatch3DCoverImage).mockResolvedValue({
      item: nextProfile,
      coverImageSrc:
        '/generated-match3d-assets/session/profile/cover/task/cover.png',
      coverImageObjectKey:
        'generated-match3d-assets/session/profile/cover/task/cover.png',
      prompt: '草莓抓大鹅封面图',
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onSaved={onSaved}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '发布' }));
    const publishDialog = screen.getByRole('dialog', {
      name: '发布抓大鹅作品',
    });
    fireEvent.click(
      within(publishDialog).getByRole('button', { name: '引用草莓' }),
    );
    fireEvent.change(within(publishDialog).getByLabelText('封面描述'), {
      target: { value: '草莓抓大鹅封面图' },
    });
    fireEvent.click(
      within(publishDialog).getByRole('button', { name: '生成封面图' }),
    );

    await waitFor(() => {
      expect(
        match3dWorksService.generateMatch3DCoverImage,
      ).toHaveBeenCalledWith(profile.profileId, {
        prompt: '草莓抓大鹅封面图',
        referenceImageSrcs: [
          'generated-match3d-assets/session/profile/items/i1/image.png',
        ],
        uploadedImageSrc: null,
      });
      expect(onSaved).toHaveBeenCalledWith(nextProfile);
      expect(
        screen.getByRole('dialog', { name: '发布抓大鹅作品' }),
      ).toBeTruthy();
    });
  });

  test('生成封面图只更新封面字段，不用旧回包覆盖当前物品素材和配置', async () => {
    const generatedItemAssets = [createReadyGeneratedItemAsset(1)];
    const profile = createProfile({
      clearCount: 12,
      difficulty: 4,
      generatedItemAssets,
    });
    const staleResponseProfile = createProfile({
      ...profile,
      coverImageSrc:
        '/generated-match3d-assets/session/profile/cover/task/cover.png',
      clearCount: 8,
      difficulty: 2,
      generatedItemAssets: [],
    });
    const onSaved = vi.fn();
    vi.mocked(match3dWorksService.generateMatch3DCoverImage).mockResolvedValue({
      item: staleResponseProfile,
      coverImageSrc:
        '/generated-match3d-assets/session/profile/cover/task/cover.png',
      coverImageObjectKey:
        'generated-match3d-assets/session/profile/cover/task/cover.png',
      prompt: '水果封面图',
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onSaved={onSaved}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '发布' }));
    const publishDialog = screen.getByRole('dialog', {
      name: '发布抓大鹅作品',
    });
    fireEvent.change(within(publishDialog).getByLabelText('封面描述'), {
      target: { value: '水果封面图' },
    });
    fireEvent.click(
      within(publishDialog).getByRole('button', { name: '生成封面图' }),
    );

    await waitFor(() => {
      expect(onSaved).toHaveBeenCalledWith(
        expect.objectContaining({
          coverImageSrc:
            '/generated-match3d-assets/session/profile/cover/task/cover.png',
          clearCount: 12,
          difficulty: 4,
          generatedItemAssets: expect.arrayContaining([
            expect.objectContaining({
              itemId: 'match3d-item-1',
              imageViews: expect.arrayContaining([
                expect.objectContaining({ viewId: 'view-01' }),
              ]),
            }),
          ]),
        }),
      );
    });
  });

  test('封面图上传后对齐拼图入口显示 AI 重绘开关和删除按钮', async () => {
    const uploadedDataUrl = 'data:image/png;base64,match3d-cover-uploaded';
    stubMatch3DCoverUpload(uploadedDataUrl);
    const profile = createProfile();
    const nextProfile = createProfile({
      coverImageSrc:
        '/generated-match3d-assets/session/profile/cover/task/cover.png',
    });
    vi.mocked(match3dWorksService.generateMatch3DCoverImage).mockResolvedValue({
      item: nextProfile,
      coverImageSrc:
        '/generated-match3d-assets/session/profile/cover/task/cover.png',
      coverImageObjectKey:
        'generated-match3d-assets/session/profile/cover/task/cover.png',
      prompt: '保留构图，改成节日果园',
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '发布' }));
    const publishDialog = screen.getByRole('dialog', {
      name: '发布抓大鹅作品',
    });
    fireEvent.change(
      within(publishDialog).getByLabelText('上传封面图', {
        selector: 'input',
      }),
      {
        target: {
          files: [new File(['x'], 'cover.png', { type: 'image/png' })],
        },
      },
    );

    await waitFor(() => {
      expect(
        within(publishDialog).getByRole('switch', { name: 'AI重绘' }),
      ).toBeTruthy();
      expect(
        within(publishDialog).getByRole('button', { name: '移除封面图' }),
      ).toBeTruthy();
      expect(within(publishDialog).getByLabelText('AI重绘要求')).toBeTruthy();
    });
    expect(within(publishDialog).queryByText('参考图')).toBeNull();

    fireEvent.change(within(publishDialog).getByLabelText('AI重绘要求'), {
      target: { value: '保留构图，改成节日果园' },
    });
    fireEvent.click(
      within(publishDialog).getByRole('button', { name: '生成封面图' }),
    );

    await waitFor(() => {
      expect(
        match3dWorksService.generateMatch3DCoverImage,
      ).toHaveBeenCalledWith(profile.profileId, {
        prompt: '保留构图，改成节日果园',
        uploadedImageSrc: uploadedDataUrl,
        referenceImageSrcs: [],
      });
    });
  });

  test('试玩只要求基础配置可保存，不被发布封面门槛阻断', async () => {
    const profile = createProfile();
    const onStartTestRun = vi.fn();
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: profile,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

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

    await waitFor(() => {
      expect(match3dWorksService.updateMatch3DWork).toHaveBeenCalledWith(
        'match3d-profile-1',
        expect.objectContaining({
          clearCount: 12,
          difficulty: 4,
          gameName: '水果抓大鹅',
        }),
      );
    });
    expect(onStartTestRun).toHaveBeenCalledWith(profile, {
      itemTypeCountOverride: 1,
    });
    expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
  });

  test('试玩前保存响应缺少素材时仍把当前生成 2D 素材带入运行态', async () => {
    const generatedItemAssets = [createReadyGeneratedItemAsset(1)];
    const profile = createProfile({ generatedItemAssets });
    const savedProfile = createProfile({ generatedItemAssets: [] });
    const persistedProfile = createProfile({ generatedItemAssets });
    const onStartTestRun = vi.fn();
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: savedProfile,
    });
    vi.mocked(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).mockResolvedValue({
      item: persistedProfile,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

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

    await waitFor(() => {
      expect(
        match3dWorksService.updateMatch3DGeneratedItemAssets,
      ).toHaveBeenCalledWith(
        profile.profileId,
        expect.objectContaining({
          generatedItemAssets: [
            expect.objectContaining({
              itemId: 'match3d-item-1',
              imageViews: expect.arrayContaining([
                expect.objectContaining({ viewId: 'view-01' }),
              ]),
              status: 'image_ready',
            }),
          ],
        }),
      );
      expect(onStartTestRun).toHaveBeenCalledWith(
        expect.objectContaining({
          profileId: profile.profileId,
          generatedItemAssets: [
            expect.objectContaining({
              itemId: 'match3d-item-1',
              imageViews: expect.arrayContaining([
                expect.objectContaining({ viewId: 'view-01' }),
              ]),
              status: 'image_ready',
            }),
          ],
        }),
        {
          itemTypeCountOverride: 1,
        },
      );
    });
  });

  test('发布仍要求封面和标签数量满足门槛', () => {
    render(
      <Match3DResultView
        profile={createProfile({ tags: ['水果', '抓大鹅'] })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    const publishButton = screen.getByRole('button', { name: '发布' });
    expect(publishButton).toHaveProperty('disabled', false);

    fireEvent.click(publishButton);
    const publishDialog = screen.getByRole('dialog', {
      name: '发布抓大鹅作品',
    });
    expect(within(publishDialog).getByText('封面图不能为空。')).toBeTruthy();
    expect(
      within(publishDialog).getByText('标签数量需要在 3 到 6 个之间。'),
    ).toBeTruthy();
    expect(
      within(publishDialog).getByRole('button', { name: '发布到广场' }),
    ).toHaveProperty('disabled', true);
    fireEvent.click(
      within(publishDialog).getByRole('button', { name: '发布到广场' }),
    );
    expect(within(publishDialog).getByText('封面图不能为空。')).toBeTruthy();
    expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
  });

  test('发布要求当前难度素材都具备五个视角', () => {
    const generatedItemAssets = [
      createReadyGeneratedItemAsset(1),
      createReadyGeneratedItemAsset(2),
      {
        ...createReadyGeneratedItemAsset(3),
        imageViews: createReadyGeneratedItemAsset(3).imageViews?.slice(0, 4),
      },
    ];

    render(
      <Match3DResultView
        profile={createProfile({
          summary: '轻松消除水果',
          coverImageSrc: 'data:image/png;base64,cover',
          clearCount: 8,
          difficulty: 2,
          generatedItemAssets,
        })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    const publishButton = screen.getByRole('button', { name: '发布' });
    expect(publishButton).toHaveProperty('disabled', false);
    fireEvent.click(publishButton);
    const publishDialog = screen.getByRole('dialog', {
      name: '发布抓大鹅作品',
    });
    expect(
      within(publishDialog).getByText(
        '当前难度需要 3 种物品，已生成 2 种，请先在素材配置中补齐。',
      ),
    ).toBeTruthy();
    expect(
      within(publishDialog).getByRole('button', { name: '发布到广场' }),
    ).toHaveProperty('disabled', true);
    expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
    fireEvent.click(within(publishDialog).getByRole('button', { name: '取消' }));
    fireEvent.click(screen.getByRole('button', { name: '难度配置' }));
    expect(screen.getByText('已生成物品种类')).toBeTruthy();
    expect(screen.getAllByText('2 种').length).toBeGreaterThan(0);
  });

  test('发布前会先把当前 2D 多视角素材写回 profile', async () => {
    const generatedItemAssets = [
      createReadyGeneratedItemAsset(1),
      createReadyGeneratedItemAsset(2),
      createReadyGeneratedItemAsset(3),
      createReadyGeneratedItemAsset(4),
      createReadyGeneratedItemAsset(5),
      createReadyGeneratedItemAsset(6),
      createReadyGeneratedItemAsset(7),
      createReadyGeneratedItemAsset(8),
      createReadyGeneratedItemAsset(9),
    ];
    const profile = createProfile({
      summary: '轻松消除水果',
      coverImageSrc: 'data:image/png;base64,cover',
      generatedItemAssets,
    });
    const savedProfile = createProfile({
      ...profile,
      generatedItemAssets: [],
    });
    const onPublished = vi.fn();
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: savedProfile,
    });
    vi.mocked(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).mockResolvedValue({
      item: profile,
    });
    vi.mocked(match3dWorksService.publishMatch3DWork).mockResolvedValue({
      item: createProfile({
        ...profile,
        publicationStatus: 'published',
        generatedItemAssets: [],
      }),
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onPublished={onPublished}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '发布' }));
    fireEvent.click(
      within(
        screen.getByRole('dialog', { name: '发布抓大鹅作品' }),
      ).getByRole('button', { name: '发布到广场' }),
    );

    await waitFor(() => {
      expect(
        match3dWorksService.updateMatch3DGeneratedItemAssets,
      ).toHaveBeenCalledWith(
        profile.profileId,
        expect.objectContaining({
          generatedItemAssets: expect.arrayContaining([
            expect.objectContaining({
              itemId: 'match3d-item-1',
              imageViews: expect.arrayContaining([
                expect.objectContaining({ viewId: 'view-01' }),
              ]),
              status: 'image_ready',
            }),
          ]),
        }),
      );
      expect(match3dWorksService.publishMatch3DWork).toHaveBeenCalledWith(
        profile.profileId,
      );
      expect(onPublished).toHaveBeenCalledWith(
        expect.objectContaining({
          publicationStatus: 'published',
          generatedItemAssets: expect.arrayContaining([
            expect.objectContaining({
              itemId: 'match3d-item-1',
              imageViews: expect.arrayContaining([
                expect.objectContaining({ viewId: 'view-01' }),
              ]),
            }),
          ]),
        }),
      );
    });
    const assetPersistCallOrder = vi.mocked(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).mock.invocationCallOrder[0];
    const publishCallOrder = vi.mocked(match3dWorksService.publishMatch3DWork)
      .mock.invocationCallOrder[0];
    expect(assetPersistCallOrder).toBeDefined();
    expect(publishCallOrder).toBeDefined();
    expect(assetPersistCallOrder!).toBeLessThan(publishCallOrder!);
  });

  test('结果页提供多 Tab，物品素材点击后进入独立预览面板', () => {
    render(
      <Match3DResultView
        profile={createProfile({ themeText: '水果' })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    expect(screen.getByRole('button', { name: '作品信息' })).toBeTruthy();
    expect(screen.getByRole('button', { name: '难度配置' })).toBeTruthy();
    expect(screen.getByRole('button', { name: '素材配置' })).toBeTruthy();
    expect(screen.queryByRole('button', { name: '音乐' })).toBeNull();
    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    expect(screen.getByRole('button', { name: '物品' })).toBeTruthy();
    expect(screen.getByRole('button', { name: 'UI素材' })).toBeTruthy();
    expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
    expect(screen.getAllByRole('button', { name: /打开.+物品素材/u }))
      .toHaveLength(20);

    fireEvent.click(
      screen.getByRole('button', { name: '打开水果核心物件物品素材' }),
    );

    expect(screen.getByRole('dialog', { name: /水果核心物件/u })).toBeTruthy();
    expect(screen.getByText('素材名称')).toBeTruthy();
    expect(screen.queryByText('暂无音效')).toBeNull();
    expect(screen.queryByLabelText('生成点击音效，10泥点')).toBeNull();
    expect(screen.queryByRole('button', { name: '重新生成' })).toBeNull();
    expect(screen.queryByText('用途')).toBeNull();
  });

  test('物品素材列表支持删除单项并写回剩余素材', async () => {
    const profile = createProfile({
      generatedItemAssets: [
        {
          itemId: 'match3d-item-1',
          itemName: '草莓',
          imageSrc:
            '/generated-match3d-assets/session/profile/items/i1/image.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/items/i1/image.png',
          modelSrc: null,
          modelObjectKey: null,
          modelFileName: null,
          taskUuid: null,
          subscriptionKey: null,
          status: 'image_ready',
          error: null,
        },
        {
          itemId: 'match3d-item-2',
          itemName: '苹果',
          imageSrc:
            '/generated-match3d-assets/session/profile/items/i2/image.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/items/i2/image.png',
          modelSrc: null,
          modelObjectKey: null,
          modelFileName: null,
          taskUuid: null,
          subscriptionKey: null,
          status: 'image_ready',
          error: null,
        },
      ],
    });
    vi.mocked(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).mockResolvedValue({
      item: createProfile({
        generatedItemAssets: [profile.generatedItemAssets![1]!],
      }),
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(
      screen.getAllByRole('button', { name: '删除物品素材' })[0]!,
    );

    await waitFor(() => {
      expect(
        match3dWorksService.updateMatch3DGeneratedItemAssets,
      ).toHaveBeenCalledWith(
        profile.profileId,
        expect.objectContaining({
          generatedItemAssets: expect.arrayContaining([
            expect.objectContaining({
              itemId: 'match3d-item-2',
              itemName: '苹果',
            }),
          ]),
        }),
      );
    });
    expect(
      screen.queryByRole('button', { name: '打开草莓物品素材' }),
    ).toBeNull();
    expect(
      screen.getByRole('button', { name: '打开苹果物品素材' }),
    ).toBeTruthy();
  });

  test('批量新增物品会解析名称并调用作品素材生成接口', async () => {
    const generatedItemAssets = [
      {
        itemId: 'match3d-item-1',
        itemName: '草莓',
        imageSrc:
          '/generated-match3d-assets/session/profile/items/i1/image.png',
        imageObjectKey:
          'generated-match3d-assets/session/profile/items/i1/image.png',
        modelSrc:
          '/generated-match3d-assets/session/profile/items/i1/model/model.glb',
        modelObjectKey:
          'generated-match3d-assets/session/profile/items/i1/model/model.glb',
        modelFileName: 'model.glb',
        taskUuid: 'task-1',
        subscriptionKey: 'sub-1',
        status: 'model_ready',
        error: null,
      },
    ];
    const profile = createProfile({ generatedItemAssets: [] });
    const onSaved = vi.fn();
    vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockResolvedValue({
      item: createProfile({ generatedItemAssets }),
      generatedItemAssets,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onSaved={onSaved}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '批量新增' }));
    fireEvent.change(screen.getByLabelText('物品名称 1'), {
      target: { value: '草莓' },
    });
    fireEvent.click(screen.getByRole('button', { name: '新增物品名称' }));
    fireEvent.change(screen.getByLabelText('物品名称 2'), {
      target: { value: '苹果' },
    });
    fireEvent.click(screen.getByRole('button', { name: '新增物品名称' }));
    fireEvent.change(screen.getByLabelText('物品名称 3'), {
      target: { value: '蓝莓' },
    });
    fireEvent.click(screen.getByRole('button', { name: '新增物品名称' }));
    fireEvent.change(screen.getByLabelText('物品名称 4'), {
      target: { value: '苹果' },
    });
    expect(
      screen.getByRole('button', { name: /生成物品素材 · 2泥点/u }),
    ).toBeTruthy();
    fireEvent.click(screen.getByRole('button', { name: /生成物品素材/u }));
    confirmPointCost();

    await waitFor(() => {
      expect(
        match3dWorksService.generateMatch3DItemAssets,
      ).toHaveBeenCalledWith(profile.profileId, {
        itemNames: ['草莓', '苹果', '蓝莓'],
      });
      expect(onSaved).toHaveBeenCalledWith(
        expect.objectContaining({ generatedItemAssets }),
      );
    });
    expect(screen.getByRole('dialog', { name: '批量新增物品' })).toBeTruthy();
    fireEvent.click(screen.getByRole('button', { name: '关闭' }));
    expect(screen.queryByRole('dialog', { name: '批量新增物品' })).toBeNull();
    expect(
      screen.getByRole('button', { name: '打开草莓物品素材' }),
    ).toBeTruthy();
  });

  test('批量新增面板关闭后素材列表继续显示生成进度', async () => {
    const deferred = createDeferred<{
      item: Match3DWorkProfile;
      generatedItemAssets: NonNullable<
        Match3DWorkProfile['generatedItemAssets']
      >;
    }>();
    vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockReturnValue(
      deferred.promise,
    );

    render(
      <Match3DResultView
        profile={createProfile({ generatedItemAssets: [] })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '批量新增' }));
    fireEvent.change(screen.getByLabelText('物品名称 1'), {
      target: { value: '草莓' },
    });
    fireEvent.click(screen.getByRole('button', { name: /生成物品素材/u }));
    confirmPointCost();

    await waitFor(() => {
      expect(screen.getAllByText('生成中').length).toBeGreaterThan(0);
    });
    fireEvent.click(screen.getByRole('button', { name: '关闭' }));

    expect(screen.queryByRole('dialog', { name: '批量新增物品' })).toBeNull();
    expect(screen.getByLabelText('物品素材生成进度')).toBeTruthy();

    deferred.resolve({
      item: createProfile({
        generatedItemAssets: [createReadyGeneratedItemAsset(1)],
      }),
      generatedItemAssets: [createReadyGeneratedItemAsset(1)],
    });

    await waitFor(() => {
      expect(screen.getByText('生成完成')).toBeTruthy();
      expect(
        screen.getByRole('button', { name: '打开物品1物品素材' }),
      ).toBeTruthy();
    });
  });

  test('批量重新生成会收集已有物品名称并按替换模式调用素材生成接口', async () => {
    const generatedItemAssets = [
      { ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
      { ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
    ];
    const regeneratedAssets = [
      {
        ...generatedItemAssets[0]!,
        imageSrc:
          '/generated-match3d-assets/session/profile/items/item-1/new-image.png',
      },
      generatedItemAssets[1]!,
    ];
    const profile = createProfile({ generatedItemAssets });
    const onSaved = vi.fn();
    vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockResolvedValue({
      item: createProfile({ generatedItemAssets: regeneratedAssets }),
      generatedItemAssets: regeneratedAssets,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onSaved={onSaved}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '批量重新生成' }));
    expect(
      screen.getByRole('dialog', { name: '批量重新生成物品' }),
    ).toBeTruthy();
    expect(screen.getByLabelText('重新生成物品名称 1')).toHaveProperty(
      'value',
      '草莓',
    );
    expect(screen.getByLabelText('重新生成物品名称 2')).toHaveProperty(
      'value',
      '苹果',
    );
    fireEvent.change(screen.getByLabelText('重新生成物品名称 2'), {
      target: { value: '' },
    });
    fireEvent.click(screen.getByRole('button', { name: /重新生成物品素材/u }));
    confirmPointCost();

    await waitFor(() => {
      expect(
        match3dWorksService.generateMatch3DItemAssets,
      ).toHaveBeenCalledWith(profile.profileId, {
        itemNames: ['草莓'],
        mode: 'replace',
      });
      expect(onSaved).toHaveBeenCalledWith(
        expect.objectContaining({ generatedItemAssets: regeneratedAssets }),
      );
      expect(
        screen.getAllByText('已重新生成 1 种物品素材').length,
      ).toBeGreaterThan(0);
    });
    expect(
      screen.getByRole('button', { name: '打开草莓物品素材' }),
    ).toBeTruthy();
  });

  test('批量重新生成只提交能匹配到的已有物品名称', async () => {
    const generatedItemAssets = [
      { ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
      { ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
      { ...createReadyGeneratedItemAsset(3), itemName: '梨子' },
      { ...createReadyGeneratedItemAsset(4), itemName: '香蕉' },
      { ...createReadyGeneratedItemAsset(5), itemName: '葡萄' },
      { ...createReadyGeneratedItemAsset(6), itemName: '橙子' },
    ];
    const profile = createProfile({ generatedItemAssets });
    vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockResolvedValue({
      item: profile,
      generatedItemAssets,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '批量重新生成' }));
    fireEvent.change(screen.getByLabelText('重新生成物品名称 1'), {
      target: { value: '草莓' },
    });
    fireEvent.change(screen.getByLabelText('重新生成物品名称 2'), {
      target: { value: '不存在' },
    });
    fireEvent.change(screen.getByLabelText('重新生成物品名称 3'), {
      target: { value: '梨子' },
    });
    fireEvent.change(screen.getByLabelText('重新生成物品名称 4'), {
      target: { value: '' },
    });
    fireEvent.change(screen.getByLabelText('重新生成物品名称 5'), {
      target: { value: '' },
    });
    fireEvent.change(screen.getByLabelText('重新生成物品名称 6'), {
      target: { value: '' },
    });

    expect(
      screen.getByRole('button', { name: /重新生成物品素材 · 2泥点/u }),
    ).toBeTruthy();
    fireEvent.click(screen.getByRole('button', { name: /重新生成物品素材/u }));
    confirmPointCost();

    await waitFor(() => {
      expect(
        match3dWorksService.generateMatch3DItemAssets,
      ).toHaveBeenCalledWith(profile.profileId, {
        itemNames: ['草莓', '梨子'],
        mode: 'replace',
      });
    });
  });

  test('难度配置对齐入口页并派生消除次数与物品数量', async () => {
    const onSaved = vi.fn();
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: createProfile({ clearCount: 21, difficulty: 8 }),
    });

    render(
      <Match3DResultView
        profile={createProfile({ clearCount: 12, difficulty: 4 })}
        onBack={() => {}}
        onSaved={onSaved}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '难度配置' }));

    expect(screen.getByRole('button', { name: '轻松 8次 · 3种' })).toBeTruthy();
    const difficultySlider = screen.getByRole('slider', { name: '难度' });
    expect((difficultySlider as HTMLInputElement).value).toBe('1');
    expect(
      screen
        .getByRole('button', { name: '标准 12次 · 9种' })
        .getAttribute('aria-pressed'),
    ).toBe('true');
    expect(screen.getByText('36 件')).toBeTruthy();
    expect(screen.getAllByText('9 种').length).toBeGreaterThan(0);

    fireEvent.change(difficultySlider, { target: { value: '3' } });

    await waitFor(() => {
      expect(match3dWorksService.updateMatch3DWork).toHaveBeenCalledWith(
        'match3d-profile-1',
        expect.objectContaining({
          clearCount: 21,
          difficulty: 8,
        }),
      );
    });
    expect(screen.getByText('63 件')).toBeTruthy();
    expect(screen.getAllByText('20 种').length).toBeGreaterThan(0);
    expect(onSaved).toHaveBeenCalledWith(
      expect.objectContaining({
        clearCount: 21,
        difficulty: 8,
      }),
    );
  });

  test('试玩使用当前 2D 多视角素材进入运行态', async () => {
    const generatedItemAssets = [createReadyGeneratedItemAsset(1)];
    const profile = createProfile({ generatedItemAssets });
    const onStartTestRun = vi.fn();
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: profile,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

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

    await waitFor(() => {
      expect(onStartTestRun).toHaveBeenCalledWith(
        expect.objectContaining({
          generatedItemAssets: [
            expect.objectContaining({
              itemId: 'match3d-item-1',
              imageViews: expect.arrayContaining([
                expect.objectContaining({ viewId: 'view-01' }),
              ]),
            }),
          ],
        }),
        {
          itemTypeCountOverride: 1,
        },
      );
    });
  });

  test('结果页优先展示生成出来的 2D 视角素材', () => {
    render(
      <Match3DResultView
        profile={createProfile({
          clearCount: 3,
          generatedItemAssets: [createReadyGeneratedItemAsset(1)],
        })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '打开物品1物品素材' }));

    expect(screen.getByDisplayValue('物品1')).toBeTruthy();
    expect(
      [...document.querySelectorAll('img')].some((image) =>
        image
          .getAttribute('src')
          ?.includes(
            'generated-match3d-assets/session/profile/items/item-1/views/view-01.png',
          ),
      ),
    ).toBe(true);
  });

  test('物品详情五视角预览不混入兼容首图', () => {
    const generatedAsset = createReadyGeneratedItemAsset(1);

    render(
      <Match3DResultView
        profile={createProfile({
          clearCount: 3,
          generatedItemAssets: [
            {
              ...generatedAsset,
              imageSrc:
                '/generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
              imageObjectKey:
                'generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
            },
          ],
        })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '打开物品1物品素材' }));

    const imageSources = [...document.querySelectorAll('img')].map(
      (image) => image.getAttribute('src') ?? '',
    );
    expect(
      imageSources.some((source) => source.includes('legacy-primary.png')),
    ).toBe(false);
    expect(
      imageSources.some((source) => source.includes('views/view-05.png')),
    ).toBe(true);
  });

  test('物品详情五视角预览使用上方大图和底部缩略图栏', () => {
    render(
      <Match3DResultView
        profile={createProfile({
          clearCount: 3,
          generatedItemAssets: [createReadyGeneratedItemAsset(1)],
        })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '打开物品1物品素材' }));

    const preview = screen.getByLabelText('物品1五视角预览');
    const stage = screen.getByTestId('match3d-item-preview-stage');
    const focusImage = screen.getByTestId('match3d-item-preview-focus-image');
    const thumbnails = screen.getByTestId('match3d-item-preview-thumbnails');
    expect(stage.className).toContain('aspect-square');
    expect(stage.className).toContain('max-w-[22rem]');
    expect(focusImage.className).toContain('place-items-center');
    expect(focusImage.querySelector('img')?.className).toContain('p-3');
    expect(thumbnails.style.gridAutoColumns).toBe('calc((100% - 1.5rem) / 4)');
    expect(preview.querySelectorAll('img')).toHaveLength(6);
    expect(
      screen
        .getByRole('button', { name: '切换物品1视角3' })
        .getAttribute('aria-pressed'),
    ).toBe('true');
    expect(
      screen.queryByTestId('match3d-item-preview-focus-frame'),
    ).toBeNull();

    fireEvent.click(screen.getByRole('button', { name: '切换物品1视角5' }));
    expect(
      screen
        .getByTestId('match3d-item-preview-focus-image')
        .getAttribute('data-preview-src'),
    ).toContain('views/view-05.png');
  });

  test('草稿阶段仅有切割图片时展示 2D 素材', () => {
    render(
      <Match3DResultView
        profile={createProfile({
          clearCount: 3,
          generatedItemAssets: [
            {
              itemId: 'match3d-item-1',
              itemName: '草莓',
              imageSrc:
                '/generated-match3d-assets/session/profile/items/strawberry/image.png',
              imageObjectKey:
                'generated-match3d-assets/session/profile/items/strawberry/image.png',
              modelSrc: null,
              modelObjectKey: null,
              modelFileName: null,
              taskUuid: null,
              subscriptionKey: null,
              status: 'image_ready',
              error: null,
            },
          ],
        })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '打开草莓物品素材' }));

    expect(screen.getByDisplayValue('草莓')).toBeTruthy();
    expect(
      [...document.querySelectorAll('img')].some((image) =>
        image.getAttribute('src')?.includes('items/strawberry/image.png'),
      ),
    ).toBe(true);
    expect(screen.queryByRole('link', { name: /\.glb/u })).toBeNull();
  });

  test('重进草稿页时从持久化 profile 素材恢复 2D 素材列表', () => {
    render(
      <Match3DResultView
        profile={createProfile({
          clearCount: 3,
          generatedItemAssets: [
            {
              itemId: 'match3d-item-1',
              itemName: '草莓',
              imageSrc:
                '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
              imageObjectKey:
                'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
              modelSrc: null,
              modelObjectKey: null,
              modelFileName: null,
              taskUuid: null,
              subscriptionKey: null,
              status: 'image_ready',
              error: null,
            },
            {
              itemId: 'match3d-item-2',
              itemName: '苹果',
              imageSrc:
                '/generated-match3d-assets/session/profile/items/match3d-item-2-item/image.png',
              imageObjectKey:
                'generated-match3d-assets/session/profile/items/match3d-item-2-item/image.png',
              modelSrc: null,
              modelObjectKey: null,
              modelFileName: null,
              taskUuid: null,
              subscriptionKey: null,
              status: 'image_ready',
              error: null,
            },
          ],
        })}
        draft={null}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));

    expect(
      screen.getByRole('button', { name: '打开草莓物品素材' }),
    ).toBeTruthy();
    expect(
      screen.getByRole('button', { name: '打开苹果物品素材' }),
    ).toBeTruthy();
    expect(
      screen.queryByRole('button', { name: '打开水果核心物件物品素材' }),
    ).toBeNull();
  });

  test('素材配置 UI素材子 Tab 仅预览背景图和UI spritesheet', () => {
    render(
      <Match3DResultView
        profile={createProfile({
          backgroundPrompt: '果园主题抓大鹅竖屏背景',
          backgroundImageSrc:
            '/generated-match3d-assets/session/profile/background/background.png',
          generatedBackgroundAsset: {
            prompt: '果园主题抓大鹅竖屏背景',
            levelScenePrompt: '果园完整关卡画面',
            levelSceneImageSrc:
              '/generated-match3d-assets/session/profile/level-scene/scene.png',
            levelSceneImageObjectKey: null,
            imageSrc:
              '/generated-match3d-assets/session/profile/background/background.png',
            imageObjectKey: null,
            uiSpritesheetPrompt: 'UI spritesheet',
            uiSpritesheetImageSrc:
              '/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
            uiSpritesheetImageObjectKey: null,
            containerPrompt: null,
            containerImageSrc: null,
            containerImageObjectKey: null,
            status: 'image_ready',
            error: null,
          },
        })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));

    expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
      '/generated-match3d-assets/session/profile/background/background.png',
    );
    expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
      '/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
    );
    expect(
      screen.queryByLabelText('UI背景图画面描述提示词'),
    ).toBeNull();
    expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();

    fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
    expect(screen.getByRole('dialog', { name: 'UI预览' })).toBeTruthy();
    expect(screen.getByText('第 1 关')).toBeTruthy();
    expect(screen.getByText('抓大鹅')).toBeTruthy();
    expect(screen.getByText('1:30')).toBeTruthy();
    const previewBoard = screen.getByTestId('match3d-ui-preview-board');
    expect(previewBoard.className).toContain('bg-transparent');
    expect(previewBoard.className).not.toContain('rounded-full');
    const containerImage = document.querySelector(
      'img[src="/match3d-background-references/pot-fused-reference.png"]',
    );
    expect(containerImage).toBeTruthy();
    expect(containerImage?.className).toContain('w-[min(116vw,42rem)]');
    expect(containerImage?.className).toContain('-translate-x-1/2');
    expect(
      document.querySelector('.animate-spin, [class*="border-l-transparent"]'),
    ).toBeNull();
    expect(
      document.querySelector(
        'svg[class*="lucide-settings"], [data-lucide="settings"]',
      ),
    ).toBeTruthy();
  });

  test('素材配置 UI素材子 Tab 从物品挂载资产展示生成背景和UI spritesheet', async () => {
    const onStartTestRun = vi.fn();
    const profile = createProfile({
      backgroundPrompt: null,
      backgroundImageSrc: null,
      backgroundImageObjectKey: null,
      generatedBackgroundAsset: null,
      generatedItemAssets: [
        {
          ...createReadyGeneratedItemAsset(1),
          itemName: '草莓',
          backgroundAsset: {
            prompt: '果园背景',
            imageSrc:
              '/generated-match3d-assets/session/profile/background/background.png',
            imageObjectKey:
              'generated-match3d-assets/session/profile/background/background.png',
            uiSpritesheetPrompt: '果园UI素材',
            uiSpritesheetImageSrc:
              '/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
            uiSpritesheetImageObjectKey:
              'generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
            containerPrompt: null,
            containerImageSrc: null,
            containerImageObjectKey: null,
            status: 'image_ready',
            error: null,
          },
        },
      ],
    });
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: createProfile({ generatedItemAssets: [] }),
    });
    vi.mocked(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).mockResolvedValue({
      item: profile,
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));

    expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
      '/generated-match3d-assets/session/profile/background/background.png',
    );
    expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
      '/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
    );
    expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
    expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();

    fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
    expect(
      document.querySelector(
        'img[src="/generated-match3d-assets/session/profile/background/background.png"]',
      ),
    ).toBeTruthy();
    expect(
      document.querySelector(
        'img[src="/generated-match3d-assets/session/profile/ui-spritesheet/ui.png"]',
      ),
    ).toBeTruthy();
    expect(
      document.querySelector(
        'img[src="/match3d-background-references/pot-fused-reference.png"]',
      ),
    ).toBeTruthy();

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

    await waitFor(() => {
      expect(onStartTestRun).toHaveBeenCalledWith(
        expect.objectContaining({
          backgroundPrompt: '果园背景',
          backgroundImageSrc:
            '/generated-match3d-assets/session/profile/background/background.png',
          generatedBackgroundAsset: expect.objectContaining({
            imageSrc:
              '/generated-match3d-assets/session/profile/background/background.png',
            uiSpritesheetImageSrc:
              '/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
          }),
        }),
        {
          itemTypeCountOverride: 1,
        },
      );
    });
  });

  test('素材配置 UI素材子 Tab 预览物品spritesheet解析结果', async () => {
    match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockResolvedValue(
      Array.from({ length: 100 }, (_, index) => ({
        label: `素材${index + 1}`,
        x: index * 2,
        y: 0,
        width: 2,
        height: 2,
        sheetWidth: 200,
        sheetHeight: 2,
        imageSrc: `data:image/png;base64,item-${index + 1}`,
      })),
    );

    render(
      <Match3DResultView
        profile={createProfile({
          generatedItemAssets: [
            { ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
            { ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
          ],
          generatedBackgroundAsset: {
            prompt: '果园主题抓大鹅竖屏背景',
            imageSrc:
              '/generated-match3d-assets/session/profile/background/background.png',
            imageObjectKey: null,
            uiSpritesheetPrompt: 'UI spritesheet',
            uiSpritesheetImageSrc:
              '/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
            uiSpritesheetImageObjectKey: null,
            itemSpritesheetPrompt: '物品 spritesheet',
            itemSpritesheetImageSrc:
              '/generated-match3d-assets/session/profile/item-spritesheet/items.png',
            itemSpritesheetImageObjectKey: null,
            status: 'image_ready',
            error: null,
          },
        })}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));

    expect(screen.getByAltText('物品素材图').getAttribute('src')).toBe(
      '/generated-match3d-assets/session/profile/item-spritesheet/items.png',
    );
    await waitFor(() => {
      expect(
        screen
          .getByTestId('match3d-item-spritesheet-preview-0-0')
          .getAttribute('src'),
      ).toBe('data:image/png;base64,item-1');
      expect(
        screen
          .getByTestId('match3d-item-spritesheet-preview-1-4')
          .getAttribute('src'),
      ).toBe('data:image/png;base64,item-10');
    });
    expect(screen.getByText('草莓')).toBeTruthy();
    expect(screen.getByText('苹果')).toBeTruthy();
    expect(
      match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
    ).toHaveBeenCalledWith(
      expect.objectContaining({
        maxRegions: 100,
        source:
          '/generated-match3d-assets/session/profile/item-spritesheet/items.png',
      }),
    );
  });

  test('素材配置 UI素材子 Tab 不提供背景或容器重新生成入口', () => {
    const profile = createProfile({
      generatedItemAssets: [
        {
          itemId: 'match3d-item-1',
          itemName: '草莓',
          imageSrc:
            '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
          modelSrc: null,
          modelObjectKey: null,
          modelFileName: null,
          taskUuid: null,
          subscriptionKey: null,
          status: 'image_ready',
          error: null,
        },
      ],
      generatedBackgroundAsset: {
        prompt: '旧背景提示词',
        imageSrc:
          '/generated-match3d-assets/session/profile/background/old/background.png',
        imageObjectKey:
          'generated-match3d-assets/session/profile/background/old/background.png',
        containerPrompt: '旧容器提示词',
        containerImageSrc:
          '/generated-match3d-assets/session/profile/ui-container/old/container.png',
        containerImageObjectKey:
          'generated-match3d-assets/session/profile/ui-container/old/container.png',
        status: 'image_ready',
        error: null,
      },
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));

    expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
    expect(screen.queryByLabelText('容器形象画面描述提示词')).toBeNull();
    expect(screen.queryByRole('button', { name: '容器形象' })).toBeNull();
    expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
    expect(match3dWorksService.generateMatch3DBackgroundImage).not.toHaveBeenCalled();
    expect(match3dWorksService.generateMatch3DContainerImage).not.toHaveBeenCalled();
  });

  test('历史草稿同时带旧 draft 和 profile 素材时以 profile 多视角素材补齐试玩资产', async () => {
    const draftAsset = {
      ...createReadyGeneratedItemAsset(1),
      imageViews: [],
    };
    const profileAsset = {
      ...createReadyGeneratedItemAsset(1),
      itemName: '草莓',
    };
    const profile = createProfile({ generatedItemAssets: [profileAsset] });
    const onStartTestRun = vi.fn();
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: createProfile({ generatedItemAssets: [] }),
    });
    vi.mocked(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).mockResolvedValue({
      item: createProfile({ generatedItemAssets: [profileAsset] }),
    });

    render(
      <Match3DResultView
        profile={profile}
        draft={{
          profileId: profile.profileId,
          gameName: profile.gameName,
          themeText: profile.themeText,
          summary: profile.summary,
          tags: profile.tags,
          coverImageSrc: null,
          referenceImageSrc: null,
          clearCount: profile.clearCount,
          difficulty: profile.difficulty,
          generatedItemAssets: [draftAsset],
        }}
        onBack={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '打开草莓物品素材' }));

    expect(screen.getByDisplayValue('草莓')).toBeTruthy();
    expect(
      [...document.querySelectorAll('img')].some((image) =>
        image.getAttribute('src')?.includes('views/view-01.png'),
      ),
    ).toBe(true);

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

    await waitFor(() => {
      expect(onStartTestRun).toHaveBeenCalledWith(
        expect.objectContaining({
          generatedItemAssets: [
            expect.objectContaining({
              itemId: 'match3d-item-1',
              imageViews: expect.arrayContaining([
                expect.objectContaining({ viewId: 'view-01' }),
              ]),
              status: 'image_ready',
            }),
          ],
        }),
        {
          itemTypeCountOverride: 1,
        },
      );
    });
  });

  test('物品详情隐藏点击音效生成入口', () => {
    const profile = createProfile({
      generatedItemAssets: [
        {
          itemId: 'match3d-item-1',
          itemName: '草莓',
          imageSrc:
            '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
          modelSrc: null,
          modelObjectKey: null,
          modelFileName: null,
          taskUuid: null,
          subscriptionKey: null,
          soundPrompt: '草莓清脆点击音效',
          status: 'image_ready',
          error: null,
        },
      ],
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    fireEvent.click(screen.getByRole('button', { name: '打开草莓物品素材' }));

    expect(screen.getByRole('dialog', { name: /草莓/u })).toBeTruthy();
    expect(screen.queryByLabelText('草莓点击音效提示词')).toBeNull();
    expect(screen.queryByRole('button', { name: /生成点击音效/u })).toBeNull();
    expect(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).not.toHaveBeenCalled();
  });

  test('素材配置隐藏背景音乐子 Tab', () => {
    const profile = createProfile({
      generatedItemAssets: [
        {
          itemId: 'match3d-item-1',
          itemName: '草莓',
          imageSrc:
            '/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
          imageObjectKey:
            'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
          modelSrc: null,
          modelObjectKey: null,
          modelFileName: null,
          taskUuid: null,
          subscriptionKey: null,
          soundPrompt: '草莓点击音效',
          backgroundMusicTitle: '果园轻舞',
          backgroundMusicStyle: '轻快, 休闲',
          backgroundMusicPrompt: '果园主题循环背景音乐',
          status: 'image_ready',
          error: null,
        },
      ],
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
    expect(screen.queryByLabelText('抓大鹅背景音乐曲名')).toBeNull();
    expect(screen.queryByLabelText('抓大鹅背景音乐风格')).toBeNull();
    expect(screen.queryByLabelText('抓大鹅背景音乐提示词')).toBeNull();
    expect(screen.queryByRole('button', { name: /生成音乐/u })).toBeNull();
    expect(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).not.toHaveBeenCalled();
  });

  test('背景音乐在非首个素材时仍显示并进入试玩 profile', async () => {
    const onStartTestRun = vi.fn();
    const generatedItemAssets = [
      createReadyGeneratedItemAsset(1),
      {
        ...createReadyGeneratedItemAsset(2),
        backgroundMusicTitle: '漂浮船歌',
        backgroundMusicStyle: '轻快, 愉悦, 现代',
        backgroundMusicPrompt: '',
        backgroundMusic: {
          taskId: 'music-task-2',
          provider: 'vector-engine-suno',
          assetObjectId: 'asset-music-2',
          assetKind: 'match3d_background_music',
          audioSrc: '/generated-match3d-assets/audio/floating-song.mp3',
          prompt: '',
          title: '漂浮船歌',
          updatedAt: '2026-05-14T00:00:00.000Z',
        },
      },
    ];
    const profile = createProfile({ generatedItemAssets });
    vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
      item: createProfile({ generatedItemAssets: [] }),
    });
    vi.mocked(
      match3dWorksService.updateMatch3DGeneratedItemAssets,
    ).mockResolvedValue({
      item: createProfile({ generatedItemAssets }),
    });

    render(
      <Match3DResultView
        profile={profile}
        onBack={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
    expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();

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

    await waitFor(() => {
      expect(onStartTestRun).toHaveBeenCalledWith(
        expect.objectContaining({
          generatedItemAssets: expect.arrayContaining([
            expect.objectContaining({
              itemId: 'match3d-item-1',
              backgroundMusic: expect.objectContaining({
                audioSrc: '/generated-match3d-assets/audio/floating-song.mp3',
              }),
            }),
          ]),
        }),
        expect.objectContaining({ itemTypeCountOverride: 2 }),
      );
    });
  });
});
