// @vitest-environment jsdom

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

import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
import * as puzzleWorksService from '../../services/puzzle-works';
import { puzzleAssetClient } from '../../services/puzzle-works/puzzleAssetClient';
import { PuzzleResultView } from './PuzzleResultView';

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

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

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

vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({
  useResolvedAssetReadUrl: (src?: string | null) => ({
    resolvedUrl: src
      ? `https://signed.example.com/${src.replace(/^\/+/u, '')}`
      : '',
    isResolving: false,
    shouldResolve: Boolean(src?.trim().startsWith('/generated-')),
  }),
}));

afterEach(() => {
  vi.useRealTimers();
  vi.unstubAllGlobals();
  vi.clearAllMocks();
});

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

    readAsDataURL() {
      this.result = dataUrl;
      this.onload?.();
    }
  }

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

function createSession(
  overrides: Partial<PuzzleAgentSessionSnapshot> = {},
): PuzzleAgentSessionSnapshot {
  const anchorPack = {
    themePromise: {
      key: 'themePromise',
      label: '题材承诺',
      value: '雨夜猫咪',
      status: 'confirmed' as const,
    },
    visualSubject: {
      key: 'visualSubject',
      label: '画面主体',
      value: '屋檐下的猫',
      status: 'confirmed' as const,
    },
    visualMood: {
      key: 'visualMood',
      label: '视觉气质',
      value: '温暖',
      status: 'confirmed' as const,
    },
    compositionHooks: {
      key: 'compositionHooks',
      label: '拼图记忆点',
      value: '雨滴与灯牌',
      status: 'confirmed' as const,
    },
    tagsAndForbidden: {
      key: 'tagsAndForbidden',
      label: '标签与禁忌',
      value: '猫咪、雨夜',
      status: 'confirmed' as const,
    },
  };
  const level = {
    levelId: 'puzzle-level-1',
    levelName: '雨夜猫街',
    pictureDescription: '屋檐下的猫与暖灯街角。',
    pictureReference: null,
    uiBackgroundPrompt: null,
    uiBackgroundImageSrc: null,
    uiBackgroundImageObjectKey: null,
    levelSceneImageSrc: null,
    levelSceneImageObjectKey: null,
    uiSpritesheetImageSrc: null,
    uiSpritesheetImageObjectKey: null,
    levelBackgroundImageSrc: null,
    levelBackgroundImageObjectKey: null,
    backgroundMusic: null,
    candidates: [
      {
        candidateId: 'candidate-1',
        imageSrc: '/puzzle/candidate-1.png',
        assetId: 'asset-1',
        prompt: '雨夜猫咪',
        actualPrompt: null,
        sourceType: 'generated' as const,
        selected: true,
      },
    ],
    selectedCandidateId: 'candidate-1',
    coverImageSrc: '/puzzle/candidate-1.png',
    coverAssetId: 'asset-1',
    generationStatus: 'ready' as const,
  };
  const baseSession: PuzzleAgentSessionSnapshot = {
    sessionId: 'puzzle-session-1',
    currentTurn: 2,
    progressPercent: 88,
    stage: 'ready_to_publish',
    anchorPack,
    draft: {
      workTitle: overrides.draft?.workTitle ?? '暖灯猫街作品',
      workDescription:
        overrides.draft?.workDescription ?? '一套雨夜猫街主题拼图。',
      levelName: level.levelName,
      summary: level.pictureDescription,
      themeTags: overrides.draft?.themeTags ?? ['猫咪', '雨夜', '暖灯'],
      forbiddenDirectives: [],
      creatorIntent: null,
      anchorPack,
      candidates: level.candidates,
      selectedCandidateId: level.selectedCandidateId,
      coverImageSrc: level.coverImageSrc,
      coverAssetId: level.coverAssetId,
      generationStatus: 'ready',
      levels: [level],
      metadata: null,
      ...overrides.draft,
    },
    messages: [],
    lastAssistantReply: null,
    publishedProfileId: null,
    suggestedActions: [],
    resultPreview: null,
    updatedAt: '2026-04-26T10:00:00.000Z',
  };

  const session = {
    ...baseSession,
    resultPreview: {
      draft: baseSession.draft!,
      publishReady: true,
      blockers: [],
      qualityFindings: [],
    },
    ...overrides,
  } satisfies PuzzleAgentSessionSnapshot;

  return session;
}

function openPuzzleLevelsTab() {
  fireEvent.click(screen.getByRole('button', { name: '拼图关卡' }));
}

describe('PuzzleResultView', () => {
  test('renders level list and work info tabs without asset config tab', () => {
    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={() => {}}
        onStartTestRun={() => {}}
      />,
    );

    const workInfoTab = screen.getByRole('button', { name: '作品信息' });
    const levelsTab = screen.getByRole('button', { name: '拼图关卡' });
    expect(workInfoTab).toBeTruthy();
    expect(levelsTab).toBeTruthy();
    expect(screen.queryByRole('button', { name: '素材配置' })).toBeNull();
    expect(
      workInfoTab.compareDocumentPosition(levelsTab) &
        Node.DOCUMENT_POSITION_FOLLOWING,
    ).toBeTruthy();
    expect(screen.queryByRole('button', { name: '音乐' })).toBeNull();
    expect(screen.getByLabelText('作品名称')).toHaveProperty(
      'value',
      '暖灯猫街作品',
    );
    expect(screen.getByLabelText('作品描述')).toHaveProperty(
      'value',
      '一套雨夜猫街主题拼图。',
    );

    openPuzzleLevelsTab();
    expect(screen.getByText('雨夜猫街')).toBeTruthy();
    expect(screen.getByText('获得更多积分激励')).toBeTruthy();
  });

  test('result action bar restores draft trial entry', () => {
    const onStartTestRun = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

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

    expect(onStartTestRun).toHaveBeenCalledWith(
      expect.objectContaining({
        workTitle: '暖灯猫街作品',
        levels: [
          expect.objectContaining({
            levelId: 'puzzle-level-1',
            levelName: '雨夜猫街',
          }),
        ],
      }),
    );
  });

  test('level detail trial keeps the complete draft and only selects the target level', () => {
    const onStartTestRun = vi.fn();
    const base = createSession();
    const firstLevel = base.draft!.levels![0]!;
    const secondLevel = {
      ...firstLevel,
      levelId: 'puzzle-level-2',
      levelName: '钟楼猫街',
      pictureDescription: '发光钟楼下的猫咪。',
      candidates: [
        {
          ...firstLevel.candidates[0]!,
          candidateId: 'candidate-2',
          imageSrc: '/puzzle/candidate-2.png',
          assetId: 'asset-2',
        },
      ],
      selectedCandidateId: 'candidate-2',
      coverImageSrc: '/puzzle/candidate-2.png',
      coverAssetId: 'asset-2',
    };

    render(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            levels: [firstLevel, secondLevel],
          },
        })}
        onBack={() => {}}
        onExecuteAction={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('钟楼猫街'));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '关卡详情' })).getByRole(
        'button',
        { name: '关卡测试' },
      ),
    );

    expect(onStartTestRun).toHaveBeenCalledWith(
      expect.objectContaining({
        levels: [
          expect.objectContaining({ levelId: 'puzzle-level-1' }),
          expect.objectContaining({ levelId: 'puzzle-level-2' }),
        ],
      }),
      { levelId: 'puzzle-level-2' },
    );
  });

  test('auto saves work info and levels through one payload', async () => {
    vi.useFakeTimers();
    vi.mocked(puzzleWorksService.updatePuzzleWork).mockResolvedValue({
      item: {} as never,
    });

    render(
      <PuzzleResultView
        session={createSession()}
        profileId="puzzle-profile-session-1"
        onBack={() => {}}
        onExecuteAction={() => {}}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '作品信息' }));
    fireEvent.change(screen.getByLabelText('作品名称'), {
      target: { value: '暖灯猫街合集' },
    });

    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(puzzleWorksService.updatePuzzleWork).toHaveBeenCalledWith(
      'puzzle-profile-session-1',
      expect.objectContaining({
        workTitle: '暖灯猫街合集',
        workDescription: '一套雨夜猫街主题拼图。',
        levelName: '雨夜猫街',
        summary: '一套雨夜猫街主题拼图。',
        themeTags: ['猫咪', '雨夜', '暖灯'],
        levels: expect.arrayContaining([
          expect.objectContaining({
            levelId: 'puzzle-level-1',
            levelName: '雨夜猫街',
          }),
        ]),
      }),
    );
  });

  test('opens an independent level detail dialog for generation and test play', () => {
    const onExecuteAction = vi.fn();
    const onStartTestRun = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
        onStartTestRun={onStartTestRun}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    expect(dialog.className).toContain('max-w-[56rem]');
    expect(dialog.querySelector('.puzzle-level-detail-list')).toBeTruthy();
    fireEvent.change(within(dialog).getByLabelText('关卡名称'), {
      target: { value: '暖灯猫街' },
    });
    fireEvent.change(within(dialog).getByLabelText('画面描述'), {
      target: { value: '一只猫在雨夜灯牌下回头。' },
    });
    fireEvent.click(
      within(dialog).getByRole('button', { name: /重新生成画面/u }),
    );
    const confirmDialog = screen.getByRole('dialog', {
      name: '确认消耗泥点',
    });
    expect(within(confirmDialog).getByText('消耗 2 泥点')).toBeTruthy();
    fireEvent.click(within(confirmDialog).getByRole('button', { name: '确定' }));

    expect(onExecuteAction).toHaveBeenCalledWith({
      action: 'generate_puzzle_images',
      levelId: 'puzzle-level-1',
      promptText: '一只猫在雨夜灯牌下回头。',
      referenceImageSrc: undefined,
      imageModel: 'gpt-image-2',
      aiRedraw: true,
      referenceImageSrcs: [],
      candidateCount: 1,
      shouldAutoNameLevel: false,
      workTitle: '暖灯猫街作品',
      workDescription: '一套雨夜猫街主题拼图。',
      summary: '一套雨夜猫街主题拼图。',
      themeTags: ['猫咪', '雨夜', '暖灯'],
      levelsJson: expect.any(String),
    });
    expect(screen.getByRole('progressbar', { name: '画面生成进度' })).toBeTruthy();
    const generatePayload = onExecuteAction.mock.calls[0]![0];
    expect(JSON.parse(generatePayload.levelsJson ?? '[]')).toEqual([
      expect.objectContaining({
        levelId: 'puzzle-level-1',
        levelName: '暖灯猫街',
        pictureDescription: '一只猫在雨夜灯牌下回头。',
        generationStatus: 'generating',
      }),
    ]);
    expect(within(dialog).getByText('预计剩余 270 秒')).toBeTruthy();
    expect(
      within(dialog).queryByPlaceholderText('参考图链接或资产ID'),
    ).toBeNull();
    const levelList = screen.getByLabelText('拼图关卡列表');
    expect(within(levelList).getAllByText('生成中').length).toBeGreaterThan(0);

    const levelNameInput = within(dialog).getByLabelText('关卡名称');
    const formalImageTitle = within(dialog).getByText('画面图');
    const formalImageCard = formalImageTitle
      .closest('.creative-image-input-panel__image-field')
      ?.querySelector('.puzzle-image-upload-card');
    const pictureDescriptionInput = within(dialog).getByLabelText('画面描述');
    expect(levelNameInput.closest('.platform-subpanel')).toBeNull();
    expect(formalImageTitle.closest('.platform-subpanel')).toBeNull();
    expect(pictureDescriptionInput.closest('.platform-subpanel')).toBeNull();
    expect(formalImageCard).toBeTruthy();
    expect(formalImageCard?.className).toContain('min-h-[');
    expect(
      levelNameInput.compareDocumentPosition(formalImageTitle) &
        Node.DOCUMENT_POSITION_FOLLOWING,
    ).toBeTruthy();
    expect(
      formalImageTitle.compareDocumentPosition(pictureDescriptionInput) &
        Node.DOCUMENT_POSITION_FOLLOWING,
    ).toBeTruthy();

    fireEvent.click(within(dialog).getByRole('button', { name: /关卡测试/u }));
    expect(onStartTestRun).toHaveBeenCalledWith(
      expect.objectContaining({
        levelName: '暖灯猫街',
        summary: '一套雨夜猫街主题拼图。',
        levels: [
          expect.objectContaining({
            levelId: 'puzzle-level-1',
            levelName: '暖灯猫街',
          }),
        ],
      }),
      { levelId: 'puzzle-level-1' },
    );
  });

  test('adds and deletes levels from the list', async () => {
    vi.useFakeTimers();
    vi.mocked(puzzleWorksService.updatePuzzleWork).mockResolvedValue({
      item: {} as never,
    });

    render(
      <PuzzleResultView
        session={createSession()}
        profileId="puzzle-profile-session-1"
        onBack={() => {}}
        onExecuteAction={() => {}}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByRole('button', { name: /新增关卡/u }));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    expect(
      within(dialog).getByRole('button', { name: /生成画面/u }),
    ).toBeTruthy();
    expect(within(dialog).getByText('消耗2泥点')).toBeTruthy();
    expect(within(dialog).getByText('画面图')).toBeTruthy();
    expect(
      within(dialog).queryByRole('button', { name: /关卡测试/u }),
    ).toBeNull();
    fireEvent.click(screen.getByLabelText('关闭'));
    expect(screen.getAllByText('第2关').length).toBeGreaterThan(0);

    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(puzzleWorksService.updatePuzzleWork).toHaveBeenLastCalledWith(
      'puzzle-profile-session-1',
      expect.objectContaining({
        levels: expect.arrayContaining([
          expect.objectContaining({ levelId: 'puzzle-level-1' }),
          expect.objectContaining({ levelName: '' }),
        ]),
      }),
    );

    fireEvent.click(screen.getByLabelText('删除关卡 第2关'));
    expect(screen.queryByText('第2关')).toBeNull();

    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(puzzleWorksService.updatePuzzleWork).toHaveBeenLastCalledWith(
      'puzzle-profile-session-1',
      expect.objectContaining({
        levels: [
          expect.objectContaining({
            levelId: 'puzzle-level-1',
          }),
        ],
      }),
    );
  });

  test('generates image for a newly added level with the current levels snapshot', () => {
    vi.spyOn(Date, 'now').mockReturnValue(1_775_000_000_000);
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByRole('button', { name: /新增关卡/u }));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    fireEvent.change(within(dialog).getByLabelText('画面描述'), {
      target: { value: '新关卡里有一座发光钟楼。' },
    });
    fireEvent.click(within(dialog).getByRole('button', { name: /生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith({
      action: 'generate_puzzle_images',
      levelId: 'puzzle-level-1775000000000-2',
      promptText: '新关卡里有一座发光钟楼。',
      referenceImageSrc: undefined,
      imageModel: 'gpt-image-2',
      aiRedraw: true,
      referenceImageSrcs: [],
      candidateCount: 1,
      shouldAutoNameLevel: true,
      workTitle: '暖灯猫街作品',
      workDescription: '一套雨夜猫街主题拼图。',
      summary: '一套雨夜猫街主题拼图。',
      themeTags: ['猫咪', '雨夜', '暖灯'],
      levelsJson: expect.any(String),
    });

    const payload = onExecuteAction.mock.calls[0]![0];
    expect(JSON.parse(payload.levelsJson ?? '[]')).toEqual([
      expect.objectContaining({ levelId: 'puzzle-level-1' }),
      expect.objectContaining({
        levelId: 'puzzle-level-1775000000000-2',
        levelName: '',
        pictureDescription: '新关卡里有一座发光钟楼。',
        generationStatus: 'generating',
      }),
    ]);
  });

  test('requests automatic level naming when generating an unnamed level image', () => {
    vi.spyOn(Date, 'now').mockReturnValue(1_775_000_000_000);
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByRole('button', { name: /新增关卡/u }));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    fireEvent.change(within(dialog).getByLabelText('画面描述'), {
      target: { value: '新关卡里有一座发光钟楼。' },
    });
    fireEvent.click(within(dialog).getByRole('button', { name: /生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_images',
        levelId: 'puzzle-level-1775000000000-2',
        promptText: '新关卡里有一座发光钟楼。',
        shouldAutoNameLevel: true,
      }),
    );
  });

  test('keeps generation progress visible after closing and reopening level dialog', () => {
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );
    fireEvent.click(screen.getByLabelText('关闭'));

    expect(
      within(screen.getByLabelText('拼图关卡列表')).getAllByText('生成中')
        .length,
    ).toBeGreaterThan(0);

    fireEvent.click(screen.getByText('雨夜猫街'));
    const reopenedDialog = screen.getByRole('dialog', { name: '关卡详情' });
    expect(
      within(reopenedDialog).getByRole('progressbar', { name: '画面生成进度' }),
    ).toBeTruthy();
    expect(within(reopenedDialog).getByText('预计剩余 270 秒')).toBeTruthy();
  });

  test('allows parallel draft editing while a level image is generating but blocks publish', () => {
    const onExecuteAction = vi.fn();
    const onStartTestRun = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
        onStartTestRun={onStartTestRun}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );
    fireEvent.change(screen.getByLabelText('关卡名称'), {
      target: { value: '继续编辑的猫街' },
    });
    fireEvent.click(screen.getByRole('button', { name: /关卡测试/u }));

    expect(onStartTestRun).toHaveBeenCalledWith(
      expect.objectContaining({
        levelName: '继续编辑的猫街',
      }),
      { levelId: 'puzzle-level-1' },
    );

    fireEvent.click(screen.getByLabelText('关闭'));
    openPuzzleLevelsTab();
    fireEvent.click(screen.getByRole('button', { name: /新增关卡/u }));
    expect(screen.getByRole('dialog', { name: '关卡详情' })).toBeTruthy();
    fireEvent.click(screen.getByLabelText('关闭'));

    fireEvent.click(screen.getByRole('button', { name: /发布/u }));
    const publishDialog = screen.getByRole('dialog', { name: '发布拼图作品' });
    expect(within(publishDialog).getByText('还有关卡画面正在生成。')).toBeTruthy();
    expect(
      within(publishDialog).getByRole('button', { name: /发布到广场/u }),
    ).toHaveProperty('disabled', true);
  });

  test('asset config tab is removed and cannot trigger the legacy UI background action', () => {
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
        isBusy={false}
      />,
    );

    expect(screen.queryByRole('button', { name: '素材配置' })).toBeNull();
    expect(screen.queryByLabelText('拼图UI背景提示词')).toBeNull();
    expect(screen.queryByRole('button', { name: /生成UI背景/u })).toBeNull();

    openPuzzleLevelsTab();
    const addLevelButton = screen.getByRole('button', { name: /新增关卡/u });
    expect(addLevelButton).toHaveProperty('disabled', false);
    fireEvent.click(addLevelButton);
    expect(screen.getByRole('dialog', { name: '关卡详情' })).toBeTruthy();
    expect(onExecuteAction).not.toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_ui_background',
      }),
    );
  });

  test('keeps the current level dialog open when another level generation completes', () => {
    const base = createSession();
    const firstLevel = base.draft!.levels![0]!;
    const generatingSecondLevel = {
      ...firstLevel,
      levelId: 'puzzle-level-2',
      levelName: '第二关',
      pictureDescription: '第二关画面正在生成。',
      candidates: [],
      selectedCandidateId: null,
      coverImageSrc: null,
      coverAssetId: null,
      generationStatus: 'generating' as const,
    };
    const localThirdLevel = {
      ...firstLevel,
      levelId: 'puzzle-level-3',
      levelName: '第三关',
      pictureDescription: '第三关初稿。',
      candidates: [],
      selectedCandidateId: null,
      coverImageSrc: null,
      coverAssetId: null,
      generationStatus: 'idle' as const,
    };
    const completedSecondLevel = {
      ...generatingSecondLevel,
      candidates: [
        {
          candidateId: 'candidate-level-2',
          imageSrc: '/puzzle/level-2.png',
          assetId: 'asset-level-2',
          prompt: '第二关画面',
          actualPrompt: null,
          sourceType: 'generated' as const,
          selected: true,
        },
      ],
      selectedCandidateId: 'candidate-level-2',
      coverImageSrc: '/puzzle/level-2.png',
      coverAssetId: 'asset-level-2',
      generationStatus: 'ready' as const,
    };

    const { rerender } = render(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            levels: [firstLevel, generatingSecondLevel, localThirdLevel],
          },
        })}
        onBack={() => {}}
        onExecuteAction={() => {}}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('第三关'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    fireEvent.change(within(dialog).getByLabelText('画面描述'), {
      target: { value: '正在编辑第三关的信息。' },
    });

    rerender(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            levels: [firstLevel, completedSecondLevel],
          },
          updatedAt: '2026-05-14T10:00:00.000Z',
        })}
        onBack={() => {}}
        onExecuteAction={() => {}}
      />,
    );

    const currentDialog = screen.getByRole('dialog', { name: '关卡详情' });
    expect(within(currentDialog).getByLabelText('关卡名称')).toHaveProperty(
      'value',
      '第三关',
    );
    expect(within(currentDialog).getByLabelText('画面描述')).toHaveProperty(
      'value',
      '正在编辑第三关的信息。',
    );
    expect(within(currentDialog).queryByDisplayValue('第二关')).toBeNull();
  });

  test('publishes with work info and serialized levels', () => {
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: /发布/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '发布拼图作品' })).getByRole(
        'button',
        { name: /发布到广场/u },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'publish_puzzle_work',
        workTitle: '暖灯猫街作品',
        workDescription: '一套雨夜猫街主题拼图。',
        levelName: '雨夜猫街',
        summary: '一套雨夜猫街主题拼图。',
        themeTags: ['猫咪', '雨夜', '暖灯'],
      }),
    );
    const payload = onExecuteAction.mock.calls[0]![0];
    expect(JSON.parse(payload.levelsJson)).toEqual([
      expect.objectContaining({
        levelId: 'puzzle-level-1',
        levelName: '雨夜猫街',
      }),
    ]);
  });

  test('keeps publish dialog open and shows backend publish error', () => {
    const onExecuteAction = vi.fn();

    const { rerender } = render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: /发布/u }));
    const dialog = screen.getByRole('dialog', { name: '发布拼图作品' });
    fireEvent.click(
      within(dialog).getByRole('button', { name: /发布到广场/u }),
    );

    rerender(
      <PuzzleResultView
        session={createSession()}
        error="泥点余额不足"
        isBusy={false}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    const publishDialog = screen.getByRole('dialog', {
      name: '发布拼图作品',
    });
    expect(publishDialog).toBeTruthy();
    expect(within(publishDialog).getByText('泥点余额不足')).toBeTruthy();
  });

  test('generates six tags after work title and description are filled', () => {
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...createSession().draft!,
            workTitle: '雨夜猫街',
            workDescription: '',
            themeTags: [],
          },
          resultPreview: {
            draft: createSession().draft!,
            publishReady: false,
            blockers: [
              {
                id: 'invalid-tag-count',
                code: 'INVALID_TAG_COUNT',
                message: '正式标签数量必须在 3 到 6 之间',
              },
            ],
            qualityFindings: [],
          },
        })}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '作品信息' }));
    fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' }));
    expect(screen.getByText('请先填写作品名称和作品描述。')).toBeTruthy();
    expect(onExecuteAction).not.toHaveBeenCalled();

    fireEvent.change(screen.getByLabelText('作品描述'), {
      target: { value: '一套雨夜猫街主题拼图。' },
    });
    fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' }));

    expect(onExecuteAction).toHaveBeenCalledWith({
      action: 'generate_puzzle_tags',
      workTitle: '雨夜猫街',
      workDescription: '一套雨夜猫街主题拼图。',
      levelName: '雨夜猫街',
      summary: '一套雨夜猫街主题拼图。',
      themeTags: [],
      levelsJson: expect.any(String),
    });
  });

  test('preserves generated level asset bundle in test run draft', () => {
    const onStartTestRun = vi.fn();
    const base = createSession();
    const level = base.draft!.levels![0]!;

    render(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            levels: [
              {
                ...level,
                levelSceneImageSrc:
                  '/generated-puzzle-assets/session/level-scene.png',
                levelSceneImageObjectKey:
                  'generated-puzzle-assets/session/level-scene.png',
                uiSpritesheetImageSrc:
                  '/generated-puzzle-assets/session/ui-spritesheet.png',
                uiSpritesheetImageObjectKey:
                  'generated-puzzle-assets/session/ui-spritesheet.png',
                levelBackgroundImageSrc:
                  '/generated-puzzle-assets/session/level-background.png',
                levelBackgroundImageObjectKey:
                  'generated-puzzle-assets/session/level-background.png',
              },
            ],
          },
        })}
        onBack={() => {}}
        onExecuteAction={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

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

    expect(onStartTestRun).toHaveBeenCalledWith(
      expect.objectContaining({
        levels: [
          expect.objectContaining({
            levelSceneImageSrc:
              '/generated-puzzle-assets/session/level-scene.png',
            levelSceneImageObjectKey:
              'generated-puzzle-assets/session/level-scene.png',
            uiSpritesheetImageSrc:
              '/generated-puzzle-assets/session/ui-spritesheet.png',
            uiSpritesheetImageObjectKey:
              'generated-puzzle-assets/session/ui-spritesheet.png',
            levelBackgroundImageSrc:
              '/generated-puzzle-assets/session/level-background.png',
            levelBackgroundImageObjectKey:
              'generated-puzzle-assets/session/level-background.png',
          }),
        ],
      }),
    );
  });

  test('does not expose music or standalone UI asset controls', () => {
    const base = createSession();
    const level = base.draft!.levels![0]!;

    render(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            levels: [
              {
                ...level,
                backgroundMusic: {
                  taskId: 'music-task-1',
                  provider: 'vector-engine-suno',
                  assetObjectId: 'asset-music-1',
                  assetKind: 'puzzle_background_music',
                  audioSrc: '/generated-puzzle-assets/session/audio/music.mp3',
                  prompt: '',
                  title: '雨夜轻响',
                  updatedAt: '2026-05-12T10:00:00.000Z',
                },
              },
            ],
          },
        })}
        onBack={() => {}}
        onExecuteAction={() => {}}
      />,
    );

    expect(screen.queryByRole('button', { name: '素材配置' })).toBeNull();
    expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
    expect(screen.queryByRole('button', { name: /重新生成音乐/u })).toBeNull();
    expect(screen.queryByLabelText('拼图背景音乐')).toBeNull();
    expect(screen.queryByLabelText('拼图UI背景提示词')).toBeNull();
  });

  test('生成完成回包合并历史音乐和关卡资产后试玩使用最新资源', () => {
    const onStartTestRun = vi.fn();
    const base = createSession();
    const localLevel = {
      ...base.draft!.levels![0]!,
      generationStatus: 'generating' as const,
      levelSceneImageSrc: null,
      uiSpritesheetImageSrc: null,
      levelBackgroundImageSrc: null,
      backgroundMusic: null,
    };
    const incomingLevel = {
      ...localLevel,
      generationStatus: 'ready' as const,
      levelSceneImageSrc:
        '/generated-puzzle-assets/session/level-scene-fruit.png',
      levelSceneImageObjectKey:
        'generated-puzzle-assets/session/level-scene-fruit.png',
      uiSpritesheetImageSrc:
        '/generated-puzzle-assets/session/ui-spritesheet-fruit.png',
      uiSpritesheetImageObjectKey:
        'generated-puzzle-assets/session/ui-spritesheet-fruit.png',
      levelBackgroundImageSrc:
        '/generated-puzzle-assets/session/level-background-fruit.png',
      levelBackgroundImageObjectKey:
        'generated-puzzle-assets/session/level-background-fruit.png',
      backgroundMusic: {
        taskId: 'music-task-fruit',
        provider: 'vector-engine-suno',
        assetObjectId: 'asset-music-fruit',
        assetKind: 'puzzle_background_music',
        audioSrc: '/generated-puzzle-assets/session/audio/fruit.mp3',
        prompt: '',
        title: '水果乐园',
        updatedAt: '2026-05-14T10:00:00.000Z',
      },
    };
    const { rerender } = render(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            levels: [localLevel],
          },
        })}
        onBack={() => {}}
        onExecuteAction={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

    rerender(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            coverImageSrc: incomingLevel.coverImageSrc,
            coverAssetId: incomingLevel.coverAssetId,
            generationStatus: 'ready',
            levels: [incomingLevel],
          },
          updatedAt: '2026-05-14T10:00:00.000Z',
        })}
        onBack={() => {}}
        onExecuteAction={() => {}}
        onStartTestRun={onStartTestRun}
      />,
    );

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

    expect(onStartTestRun).toHaveBeenCalledWith(
      expect.objectContaining({
        levels: [
          expect.objectContaining({
            levelSceneImageSrc:
              '/generated-puzzle-assets/session/level-scene-fruit.png',
            uiSpritesheetImageSrc:
              '/generated-puzzle-assets/session/ui-spritesheet-fruit.png',
            levelBackgroundImageSrc:
              '/generated-puzzle-assets/session/level-background-fruit.png',
            backgroundMusic: expect.objectContaining({
              audioSrc: '/generated-puzzle-assets/session/audio/fruit.mp3',
            }),
          }),
        ],
      }),
    );
  });

  test('auto saves generated level asset bundle through levels', async () => {
    vi.useFakeTimers();
    vi.mocked(puzzleWorksService.updatePuzzleWork).mockResolvedValue({
      item: {} as never,
    });
    const base = createSession();
    const level = base.draft!.levels![0]!;

    render(
      <PuzzleResultView
        session={createSession({
          draft: {
            ...base.draft!,
            levels: [
              {
                ...level,
                levelSceneImageSrc:
                  '/generated-puzzle-assets/session/level-scene.png',
                uiSpritesheetImageSrc:
                  '/generated-puzzle-assets/session/ui-spritesheet.png',
                levelBackgroundImageSrc:
                  '/generated-puzzle-assets/session/level-background.png',
              },
            ],
          },
        })}
        profileId="puzzle-profile-session-1"
        onBack={() => {}}
        onExecuteAction={() => {}}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    fireEvent.change(screen.getByLabelText('关卡名称'), {
      target: { value: '雨夜猫街新版' },
    });

    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(puzzleWorksService.updatePuzzleWork).toHaveBeenCalledWith(
      'puzzle-profile-session-1',
      expect.objectContaining({
        levels: [
          expect.objectContaining({
            levelId: 'puzzle-level-1',
            levelName: '雨夜猫街新版',
            levelSceneImageSrc:
              '/generated-puzzle-assets/session/level-scene.png',
            uiSpritesheetImageSrc:
              '/generated-puzzle-assets/session/ui-spritesheet.png',
            levelBackgroundImageSrc:
              '/generated-puzzle-assets/session/level-background.png',
          }),
        ],
      }),
    );
  });

  test('selects a history puzzle asset as reference image for the selected level', async () => {
    const onExecuteAction = vi.fn();
    vi.mocked(puzzleAssetClient.listHistoryAssets).mockResolvedValue([
      {
        assetObjectId: 'asset-history-1',
        assetKind: 'puzzle_cover_image',
        imageSrc: '/generated-puzzle-assets/history/image.png',
        ownerUserId: 'user-1',
        ownerLabel: '账号 user-1',
        profileId: null,
        entityId: 'puzzle-session-1',
        createdAt: '1713686400.000000Z',
        updatedAt: '1713686400.000000Z',
      },
    ]);

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    const uploadInput = within(dialog).getAllByLabelText('上传参考图', {
      selector: 'input',
    })[0]!;
    expect(uploadInput.closest('.platform-subpanel')).toBeNull();
    expect(uploadInput.closest('.puzzle-level-detail-list')).toBeTruthy();
    const historyButton = within(dialog).getByRole('button', {
      name: '选择历史图片',
    });
    expect(within(historyButton).getByText('历史')).toBeTruthy();
    fireEvent.click(historyButton);

    const picker = await screen.findByRole('dialog', {
      name: '选择历史图片',
    });
    expect(await within(picker).findByText('image.png')).toBeTruthy();
    expect(await within(picker).findByText(/2024\/04\/21/u)).toBeTruthy();
    expect(within(picker).queryByText('账号 user-1')).toBeNull();
    fireEvent.click(
      await within(picker).findByRole('button', { name: /image\.png/u }),
    );

    await waitFor(() => {
      expect(screen.queryByRole('dialog', { name: '选择历史图片' })).toBeNull();
    });
    expect(within(dialog).getByAltText('拼图参考图').getAttribute('src')).toBe(
      '/generated-puzzle-assets/history/image.png',
    );

    fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );
    expect(onExecuteAction).toHaveBeenLastCalledWith({
      action: 'generate_puzzle_images',
      levelId: 'puzzle-level-1',
      promptText: '屋檐下的猫与暖灯街角。',
      referenceImageSrc: '/generated-puzzle-assets/history/image.png',
      imageModel: 'gpt-image-2',
      aiRedraw: true,
      referenceImageSrcs: [],
      candidateCount: 1,
      shouldAutoNameLevel: false,
      workTitle: '暖灯猫街作品',
      workDescription: '一套雨夜猫街主题拼图。',
      summary: '一套雨夜猫街主题拼图。',
      themeTags: ['猫咪', '雨夜', '暖灯'],
      levelsJson: expect.any(String),
    });
  });

  test('uses the saved level picture reference when regenerating a level image', () => {
    const onExecuteAction = vi.fn();
    const session = createSession({
      draft: {
        ...createSession().draft!,
        levels: [
          {
            ...createSession().draft!.levels![0]!,
            pictureReference: '/generated-puzzle-assets/history/saved-reference.png',
          },
        ],
      },
    });

    render(
      <PuzzleResultView
        session={session}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_images',
        referenceImageSrc: '/generated-puzzle-assets/history/saved-reference.png',
        aiRedraw: true,
      }),
    );
    expect(screen.queryByPlaceholderText('参考图链接或资产ID')).toBeNull();
  });

  test('passes the selected image model when regenerating a level image', () => {
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    fireEvent.click(within(dialog).getByRole('button', { name: '图片模型' }));
    fireEvent.click(
      within(dialog).getByRole('menuitemradio', { name: 'gpt-image-2' }),
    );
    fireEvent.click(
      within(dialog).getByRole('button', { name: /重新生成画面/u }),
    );
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_images',
        imageModel: 'gpt-image-2',
      }),
    );
  });

  test('level image editor exposes entrance image editing controls without sharing UI background state', () => {
    const onExecuteAction = vi.fn();
    const session = createSession({
      draft: {
        ...createSession().draft!,
        levels: [
          {
            ...createSession().draft!.levels![0]!,
            pictureReference: '/generated-puzzle-assets/history/saved-reference.png',
          },
        ],
      },
    });

    render(
      <PuzzleResultView
        session={session}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });

    expect(within(dialog).getByText('画面图')).toBeTruthy();
    expect(within(dialog).getByLabelText('上传参考图')).toBeTruthy();
    expect(within(dialog).getByRole('switch', { name: 'AI重绘' })).toHaveProperty(
      'checked',
      true,
    );
    expect(
      within(dialog).getByRole('button', { name: '选择历史图片' }),
    ).toBeTruthy();

    fireEvent.change(within(dialog).getByLabelText('画面AI重绘要求（提示词）'), {
      target: { value: '只重绘第一关猫街画面' },
    });
    fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_images',
        levelId: 'puzzle-level-1',
        promptText: '只重绘第一关猫街画面',
        aiRedraw: true,
      }),
    );
    expect(onExecuteAction).not.toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_ui_background',
      }),
    );
  });

  test('level image editor keeps AI redraw switch scoped to the level image action', () => {
    const onExecuteAction = vi.fn();
    const session = createSession({
      draft: {
        ...createSession().draft!,
        levels: [
          {
            ...createSession().draft!.levels![0]!,
            pictureReference: '/generated-puzzle-assets/history/saved-reference.png',
          },
        ],
      },
    });

    render(
      <PuzzleResultView
        session={session}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });

    fireEvent.click(within(dialog).getByRole('switch', { name: 'AI重绘' }));
    fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_images',
        levelId: 'puzzle-level-1',
        aiRedraw: false,
      }),
    );
    expect(onExecuteAction).not.toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_ui_background',
        aiRedraw: false,
      }),
    );
  });

  test('level image editor submits uploaded image directly when AI redraw is off', async () => {
    const onExecuteAction = vi.fn();
    const uploadedDataUrl = 'data:image/png;base64,level-upload';
    stubReferenceImageUpload(uploadedDataUrl);

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    const uploadInput = within(dialog).getAllByLabelText('上传参考图', {
      selector: 'input',
    })[0]!;
    fireEvent.change(uploadInput, {
      target: {
        files: [new File(['x'], 'level-upload.png', { type: 'image/png' })],
      },
    });

    await waitFor(() => {
      expect(within(dialog).getByRole('switch', { name: 'AI重绘' })).toBeTruthy();
    });
    expect(within(dialog).getByAltText('拼图参考图')).toHaveProperty(
      'src',
      uploadedDataUrl,
    );
    fireEvent.click(within(dialog).getByRole('switch', { name: 'AI重绘' }));
    fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_images',
        levelId: 'puzzle-level-1',
        referenceImageSrc: uploadedDataUrl,
        referenceImageSrcs: [],
        aiRedraw: false,
      }),
    );
  });

  test('level image editor uploads prompt reference images from the description box', async () => {
    const onExecuteAction = vi.fn();
    const uploadedDataUrl = 'data:image/png;base64,level-reference';
    stubReferenceImageUpload(uploadedDataUrl);

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });
    const referenceInputs = within(dialog).getAllByLabelText('上传参考图', {
      selector: 'input',
    });
    expect(referenceInputs.length).toBeGreaterThanOrEqual(2);
    fireEvent.change(referenceInputs[referenceInputs.length - 1]!, {
      target: {
        files: [new File(['x'], 'prompt-reference.png', { type: 'image/png' })],
      },
    });

    await waitFor(() => {
      expect(
        within(dialog).getByRole('button', {
          name: /预览参考图 prompt-reference\.png/u,
        }),
      ).toBeTruthy();
    });
    fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
    fireEvent.click(
      within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
        'button',
        { name: '确定' },
      ),
    );

    expect(onExecuteAction).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'generate_puzzle_images',
        levelId: 'puzzle-level-1',
        referenceImageSrc: undefined,
        referenceImageSrcs: [uploadedDataUrl],
        aiRedraw: true,
      }),
    );
  });

  test('level image editor hides AI redraw controls when only the formal image is shown', () => {
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    openPuzzleLevelsTab();
    fireEvent.click(screen.getByText('雨夜猫街'));
    const dialog = screen.getByRole('dialog', { name: '关卡详情' });

    expect(within(dialog).queryByRole('switch', { name: 'AI重绘' })).toBeNull();
    expect(within(dialog).getByLabelText('画面描述')).toBeTruthy();
  });

  test('standalone UI background generator stays removed from the result page', () => {
    const onExecuteAction = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={onExecuteAction}
      />,
    );

    expect(screen.queryByRole('button', { name: '素材配置' })).toBeNull();
    expect(screen.queryByText('UI背景预览')).toBeNull();
    expect(screen.queryByLabelText('UI背景提示词')).toBeNull();
    expect(screen.queryByRole('button', { name: /生成UI背景/u })).toBeNull();
    expect(onExecuteAction).not.toHaveBeenCalled();
  });

  test('shows creative agent draft edit bar and submits the current draft', () => {
    const onSubmit = vi.fn();

    render(
      <PuzzleResultView
        session={createSession()}
        onBack={() => {}}
        onExecuteAction={() => {}}
        creativeDraftEdit={{
          isBusy: false,
          error: null,
          onSubmit,
        }}
      />,
    );

    fireEvent.change(screen.getByLabelText('智能修订拼图草稿'), {
      target: { value: '把标题改得轻松一点' },
    });
    fireEvent.click(screen.getByRole('button', { name: '修改' }));

    expect(onSubmit).toHaveBeenCalledWith({
      instruction: '把标题改得轻松一点',
      currentDraft: expect.objectContaining({
        workTitle: '暖灯猫街作品',
        workDescription: '一套雨夜猫街主题拼图。',
        levels: [
          expect.objectContaining({
            levelId: 'puzzle-level-1',
            pictureReference: null,
          }),
        ],
      }),
    });
  });
});
