import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
import { ImageCanvas } from '@/pages/ImageCanvas';
import { useAuthStore } from '@/stores/auth';
import { useImagePromptMuseumStore } from '@/stores/image-prompt-museum';
import { useImageWorkspaceStore } from '@/stores/image-workspace';
import type {
  DesignAssistantDeltaEvent,
  DesignConversation,
  DesignGenerationQuote,
  DesignGenerationTask,
  DesignGenerationTaskUpdatedEvent,
  DesignWorkspace,
  DesignWorkspaceBootstrap,
} from '../../shared/image-workspace';

const fetchImageWorkspaceMock = vi.hoisted(() => vi.fn());
const fetchImageWorkspaceProjectMock = vi.hoisted(() => vi.fn());
const fetchImageWorkspaceConversationMock = vi.hoisted(() => vi.fn());
const fetchImageWorkspaceTasksMock = vi.hoisted(() => vi.fn());
const sendImageWorkspaceMessageMock = vi.hoisted(() => vi.fn());
const confirmImageWorkspaceGenerationMock = vi.hoisted(() => vi.fn());
const updateImageWorkspaceGenerationQuoteMock = vi.hoisted(() => vi.fn());
const openImageWorkspaceTaskEventsMock = vi.hoisted(() => vi.fn());
const resolveImageWorkspaceAssetUrlMock = vi.hoisted(() => vi.fn());
const saveImageWorkspaceAssetMock = vi.hoisted(() => vi.fn());
const uploadImageWorkspaceAssetMock = vi.hoisted(() => vi.fn());

type EventListener = (event: MessageEvent<string>) => void;

class MockEventSource {
  onopen: ((event: Event) => void) | null = null;
  onerror: ((event: Event) => void) | null = null;
  readonly close = vi.fn();
  private readonly listeners = new Map<string, Set<EventListener>>();

  addEventListener(type: string, listener: EventListener): void {
    const listeners = this.listeners.get(type) ?? new Set<EventListener>();
    listeners.add(listener);
    this.listeners.set(type, listeners);
  }

  emit(type: string, payload: unknown): void {
    const event = { data: JSON.stringify(payload) } as MessageEvent<string>;
    for (const listener of this.listeners.get(type) ?? []) listener(event);
  }
}

function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
  let resolve!: (value: T) => void;
  const promise = new Promise<T>((accept) => {
    resolve = accept;
  });
  return { promise, resolve };
}

vi.mock('@/lib/image-workspace', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@/lib/image-workspace')>();
  return {
    ...actual,
    fetchImageWorkspace: (...args: unknown[]) => fetchImageWorkspaceMock(...args),
    fetchImageWorkspaceProject: (...args: unknown[]) => fetchImageWorkspaceProjectMock(...args),
    fetchImageWorkspaceConversation: (...args: unknown[]) => (
      fetchImageWorkspaceConversationMock(...args)
    ),
    fetchImageWorkspaceTasks: (...args: unknown[]) => fetchImageWorkspaceTasksMock(...args),
    sendImageWorkspaceMessage: (...args: unknown[]) => sendImageWorkspaceMessageMock(...args),
    confirmImageWorkspaceGeneration: (...args: unknown[]) => (
      confirmImageWorkspaceGenerationMock(...args)
    ),
    updateImageWorkspaceGenerationQuote: (...args: unknown[]) => (
      updateImageWorkspaceGenerationQuoteMock(...args)
    ),
    openImageWorkspaceTaskEvents: (...args: unknown[]) => (
      openImageWorkspaceTaskEventsMock(...args)
    ),
    resolveImageWorkspaceAssetUrl: (...args: unknown[]) => (
      resolveImageWorkspaceAssetUrlMock(...args)
    ),
    saveImageWorkspaceAsset: (...args: unknown[]) => (
      saveImageWorkspaceAssetMock(...args)
    ),
    uploadImageWorkspaceAsset: (...args: unknown[]) => (
      uploadImageWorkspaceAssetMock(...args)
    ),
  };
});

const bootstrapFixture: DesignWorkspaceBootstrap = {
  capabilities: {
    conversation: true,
    generation: true,
    image: true,
    video: true,
  },
  workspaces: [{
    workspaceId: 'workspace-cloud',
    title: '云端角色设定',
    viewRevision: 1,
    conversationCount: 1,
    phase: 'awaiting_confirmation',
    updatedAt: '2026-07-31T10:00:00Z',
  }],
};

function conversationFixture(): DesignConversation {
  return {
    conversationId: 'conversation-cloud',
    workspaceId: 'workspace-cloud',
    title: '主会话',
    turnRevision: 1,
    phase: 'awaiting_confirmation',
    brief: {
      version: 1,
      status: 'ready',
      medium: 'image',
      summary: '夜色中的机械城堡角色海报',
      ready: true,
      missingDecision: null,
    },
    createdAt: '2026-07-31T10:00:00Z',
    updatedAt: '2026-07-31T10:00:00Z',
    messages: [
      {
        id: 'message-user',
        role: 'user',
        kind: 'user',
        text: '做一张夜色中的机械城堡角色海报',
        quickReplies: [],
        generationQuote: null,
        turnRevision: 1,
        createdAt: '2026-07-31T10:00:00Z',
      },
      {
        id: 'message-assistant',
        role: 'assistant',
        kind: 'confirmation',
        text: '方向已经明确，确认后开始生成。',
        quickReplies: ['确认生成', '继续调整'],
        generationQuote: {
          quoteId: 'quote-one',
          status: 'active',
          medium: 'image',
          briefVersion: 1,
          briefSummary: '夜色中的机械城堡角色海报',
          finalPrompt: '夜色中的机械城堡角色海报，电影感构图。',
          promptMode: 'guided',
          generationParameters: {
            model: 'image-model-one',
            resolution: '1024x1024',
            aspectRatio: '1:1',
            durationSeconds: null,
          },
          generationOptions: {
            models: [{
              value: 'image-model-one',
              label: '图片模型一',
              default: true,
              disabled: false,
              multiplier: 1,
            }],
            resolutions: [{
              value: '1024x1024',
              label: '1024 × 1024',
              default: true,
              disabled: false,
              multiplier: 1,
            }, {
              value: '2048x2048',
              label: '2048 × 2048',
              default: false,
              disabled: false,
              multiplier: 1.5,
            }],
            durations: [],
            aspectRatios: [{
              value: '1:1',
              label: '1:1',
              default: true,
              disabled: false,
              multiplier: 1,
            }, {
              value: '16:9',
              label: '16:9',
              default: false,
              disabled: false,
              multiplier: 1,
            }],
          },
          pricing: {
            schema: 'design-pricing-v2',
            amount: 1,
            rounding: 'ceil',
          },
          quotedDesignPoints: 1,
          expiresAt: '2026-07-31T11:00:00Z',
        },
        turnRevision: 1,
        createdAt: '2026-07-31T10:00:01Z',
      },
    ],
  };
}

function workspaceFixture(): DesignWorkspace {
  const { messages: _messages, ...summary } = conversationFixture();
  return {
    ...bootstrapFixture.workspaces[0],
    conversations: [summary],
  };
}

const taskFixture: DesignGenerationTask = {
  taskId: 'task-one',
  workspaceId: 'workspace-cloud',
  conversationId: 'conversation-cloud',
  medium: 'image',
  status: 'succeeded',
  briefVersion: 1,
  briefSummary: '夜色中的机械城堡角色海报',
  quoteId: 'quote-old',
  quotedDesignPoints: 1,
  failureCode: null,
  resultAssets: [{
    assetId: 'asset-one',
    workspaceId: 'workspace-cloud',
    mediaType: 'image',
    mimeType: 'image/png',
    width: 1024,
    height: 1024,
    durationMilliseconds: null,
    createdAt: '2026-07-31T09:00:00Z',
    contentPath: '/api/works/image-workspace/workspaces/workspace-cloud/assets/asset-one/content',
  }],
  createdAt: '2026-07-31T09:00:00Z',
  updatedAt: '2026-07-31T09:01:00Z',
};

describe('ImageCanvas Workspace-first design experience', () => {
  let taskEventSource: MockEventSource;

  beforeEach(() => {
    vi.clearAllMocks();
    useImageWorkspaceStore.getState().reset();
    useImagePromptMuseumStore.getState().clearPendingPrompt();
    fetchImageWorkspaceMock.mockResolvedValue(bootstrapFixture);
    fetchImageWorkspaceProjectMock.mockResolvedValue(workspaceFixture());
    fetchImageWorkspaceConversationMock.mockResolvedValue(conversationFixture());
    fetchImageWorkspaceTasksMock.mockResolvedValue([taskFixture]);
    sendImageWorkspaceMessageMock.mockResolvedValue(conversationFixture());
    updateImageWorkspaceGenerationQuoteMock.mockImplementation(async (
      _workspaceId: string,
      _quoteId: string,
      finalPrompt: string,
      generationParameters: DesignGenerationQuote['generationParameters'],
    ) => ({
      ...conversationFixture().messages[1].generationQuote!,
      finalPrompt,
      generationParameters,
      quotedDesignPoints: 3,
      pricing: {
        schema: 'design-pricing-v2',
        amount: 3,
        rounding: 'ceil',
      },
    }));
    confirmImageWorkspaceGenerationMock.mockResolvedValue({
      ...conversationFixture(),
      turnRevision: 2,
      phase: 'shaping',
    });
    taskEventSource = new MockEventSource();
    openImageWorkspaceTaskEventsMock.mockResolvedValue(
      taskEventSource as unknown as EventSource,
    );
    resolveImageWorkspaceAssetUrlMock.mockResolvedValue(
      'http://127.0.0.1:13210/content?token=host',
    );
    saveImageWorkspaceAssetMock.mockResolvedValue({ status: 'saved' });
    uploadImageWorkspaceAssetMock.mockResolvedValue({
      ...taskFixture.resultAssets[0],
      assetId: 'asset-uploaded',
    });
  });

  it('shows an honest unavailable state without fabricating projects', async () => {
    fetchImageWorkspaceMock.mockRejectedValueOnce(new ImageWorkspaceApiError(
      501,
      'IMAGE_WORKSPACE_UNAVAILABLE',
      'AI 设计暂不可用',
    ));

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    expect(await screen.findByTestId('image-workspace-unavailable'))
      .toHaveTextContent('AI 设计暂不可用');
    expect(screen.getByText(/暂时无法连接设计服务/)).toBeInTheDocument();
  });

  it('clears stale renderer auth when Main requires authentication', async () => {
    useAuthStore.setState({
      initialized: true,
      loading: false,
      error: null,
      accessToken: 'expired-access-token',
      canRefresh: true,
      legacyRefreshToken: 'invalid-refresh-token',
      tokenType: 'Bearer',
      expiresAt: Date.now() - 1,
      user: {
        username: 'brother7',
        userId: '1',
        tenantId: null,
        deptId: null,
        authorities: [],
      },
    });
    fetchImageWorkspaceMock.mockRejectedValueOnce(new ImageWorkspaceApiError(
      401,
      'AUTH_REQUIRED',
      '请先登录后再使用 AI 设计',
    ));

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    await screen.findByTestId('image-workspace-unavailable');
    expect(useAuthStore.getState()).toMatchObject({
      accessToken: null,
      canRefresh: false,
      legacyRefreshToken: null,
      user: null,
    });
    expect(useImageWorkspaceStore.getState().status).toBe('auth-required');

    useAuthStore.setState({
      accessToken: 'fresh-access-token',
      canRefresh: true,
      legacyRefreshToken: 'fresh-refresh-token',
      tokenType: 'Bearer',
      expiresAt: Date.now() + 60_000,
      user: {
        username: 'brother7',
        userId: '1',
        tenantId: null,
        deptId: null,
        authorities: [],
      },
    });

    await waitFor(() => expect(fetchImageWorkspaceMock).toHaveBeenCalledTimes(2));
    await waitFor(() => expect(useImageWorkspaceStore.getState().status).toBe('ready'));
  });

  it('renders the conversation messages edge-to-edge with the unified image/video task list', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    expect(await screen.findByTestId('image-workspace-conversation')).toBeInTheDocument();
    expect(screen.getByTestId('image-canvas-page')).toHaveClass('font-sans');
    expect(screen.queryByTestId('image-canvas-conversation-select')).not.toBeInTheDocument();
    expect(screen.getByTestId('image-workspace-conversation'))
      .toHaveTextContent('方向已经明确');
    expect(screen.queryByText('设计 Agent', { exact: true })).not.toBeInTheDocument();
    expect(screen.queryByText('你', { exact: true })).not.toBeInTheDocument();
    const messageRows = screen.getAllByTestId('image-workspace-message');
    expect(messageRows).toHaveLength(2);
    expect(messageRows[0]).toHaveClass('w-full');
    expect(messageRows[1]).toHaveClass('w-full');
    expect(messageRows[0].firstElementChild).toHaveClass('w-fit', 'max-w-[85%]');
    expect(messageRows[0].querySelector('p')).toHaveClass('chat-message-body');
    expect(messageRows[1].querySelector('p')).toHaveClass('chat-message-body');
    expect(screen.getByTestId('design-quote-quote-one')).toHaveTextContent('1 设计点');
    expect(screen.queryByTestId('design-quote-model')).not.toBeInTheDocument();
    expect(screen.getByTestId('design-task-list')).toHaveTextContent('生成内容');
    expect(screen.queryByText('图片与视频任务统一展示')).not.toBeInTheDocument();
    const taskCard = screen.getByTestId('design-task-task-one');
    expect(taskCard).toHaveTextContent('生图');
    expect(taskCard.querySelector('time')).toHaveAttribute('datetime', taskFixture.createdAt);
    expect(taskCard).not.toHaveTextContent('已完成');
    expect(screen.queryByRole('button', { name: '下载原图' })).not.toBeInTheDocument();
    await waitFor(() => expect(screen.getByRole('img', { name: 'AI 设计生成缩略图' }))
      .toHaveAttribute('src', 'http://127.0.0.1:13210/content?token=host'));
    expect(screen.queryByText('生成设置')).not.toBeInTheDocument();
    expect(screen.queryByText(/当前 Agent/)).not.toBeInTheDocument();
  });

  it('keeps task cards fixed-size and truncates long descriptions', async () => {
    const longDescription = '夜色中的机械城堡角色海报，加入冷暖对比和电影感轮廓光效果';
    const compactDescription = `${Array.from(longDescription).slice(0, 24).join('')}…`;
    fetchImageWorkspaceTasksMock.mockResolvedValueOnce([{
      ...taskFixture,
      taskId: 'task-long-description',
      briefSummary: longDescription,
    }]);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const taskCard = await screen.findByTestId('design-task-task-long-description');
    expect(taskCard).toHaveClass('h-20', 'w-full');
    expect(within(taskCard).getByRole('button')).toHaveClass('h-full');
    expect(taskCard).toHaveTextContent(compactDescription);
    expect(taskCard).not.toHaveTextContent(longDescription);
  });

  it('edits the final prompt and server-provided options before re-quoting', async () => {
    const requote = deferred<DesignGenerationQuote>();
    updateImageWorkspaceGenerationQuoteMock.mockReset();
    updateImageWorkspaceGenerationQuoteMock.mockReturnValueOnce(requote.promise);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const finalPrompt = await screen.findByTestId('design-quote-final-prompt');
    fireEvent.change(finalPrompt, { target: { value: '用户最新编辑后的完整提示词' } });
    fireEvent.change(screen.getByTestId('design-quote-resolution'), {
      target: { value: '2048x2048' },
    });
    fireEvent.change(screen.getByTestId('design-quote-aspect-ratio'), {
      target: { value: '16:9' },
    });

    await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'quote-one',
      '用户最新编辑后的完整提示词',
      {
        model: 'image-model-one',
        resolution: '2048x2048',
        aspectRatio: '16:9',
        durationSeconds: null,
      },
    ));
    expect(screen.getByRole('button', { name: '重新报价中' })).toBeDisabled();

    requote.resolve({
      ...conversationFixture().messages[1].generationQuote!,
      finalPrompt: '用户最新编辑后的完整提示词',
      generationParameters: {
        model: 'image-model-one',
        resolution: '2048x2048',
        aspectRatio: '16:9',
        durationSeconds: null,
      },
      quotedDesignPoints: 3,
      pricing: { schema: 'design-pricing-v2', amount: 3, rounding: 'ceil' },
    });

    await waitFor(() => expect(screen.getByTestId('design-quote-quote-one'))
      .toHaveTextContent('3 设计点'));
    expect(screen.getByTestId('design-quote-final-prompt'))
      .toHaveValue('用户最新编辑后的完整提示词');
    const quoteCard = screen.getByTestId('design-quote-quote-one');
    expect(quoteCard).toHaveTextContent('报价 3 设计点');
    expect(within(quoteCard).getByTestId('design-quote-points')).toHaveTextContent('报价 3 设计点');
    expect(within(quoteCard).queryByTestId('design-quote-model')).not.toBeInTheDocument();
    expect(quoteCard).toHaveTextContent('继续调整');
    expect(quoteCard).toHaveTextContent('清晰度');
    expect(quoteCard).toHaveTextContent('画幅');
    expect(quoteCard).not.toHaveTextContent('提示词模式：guided');
    expect(quoteCard).not.toHaveTextContent('夜色中的机械城堡角色海报');
    expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeEnabled();
  });

  it('offers an actionable retry when re-quoting fails', async () => {
    updateImageWorkspaceGenerationQuoteMock.mockRejectedValueOnce(
      new ImageWorkspaceApiError(409, 'generation_quote_invalid', '当前生成方案已失效'),
    );

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    fireEvent.change(await screen.findByTestId('design-quote-final-prompt'), {
      target: { value: '需要重新报价的提示词' },
    });

    const quoteCard = screen.getByTestId('design-quote-quote-one');
    await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalled());
    expect(await within(quoteCard).findByRole('alert')).toHaveTextContent('当前生成方案已失效');
    expect(within(quoteCard).getByText(/请点击“重试报价”/)).toBeInTheDocument();
    expect(within(quoteCard).getByRole('button', { name: '重试报价' })).toBeEnabled();
    expect(within(quoteCard).getByRole('button', { name: '继续调整' })).toBeEnabled();
    expect(within(quoteCard).getByRole('button', { name: '等待报价更新' })).toBeDisabled();

    fireEvent.click(within(quoteCard).getByRole('button', { name: '重试报价' }));
    await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalledTimes(2));
    await waitFor(() => expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeEnabled());
  });

  it('uses the compact logo-only empty state for a new design conversation', async () => {
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
      ...conversationFixture(),
      messages: [],
    });

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const emptyState = await screen.findByTestId('image-workspace-empty-state');
    expect(emptyState).toHaveTextContent('今天一起描绘什么？');
    expect(within(emptyState).getByRole('img', { name: 'Makelore' })).toBeInTheDocument();
    expect(within(emptyState).getAllByRole('img')).toHaveLength(1);
    expect(emptyState).not.toHaveTextContent('先聊清楚，再开始创作');
    expect(emptyState).not.toHaveTextContent('告诉设计 Agent');
  });

  it('prefills the exact Prompt Museum text without sending it automatically', async () => {
    const prompt = 'Create a quiet editorial poster with generous negative space.';
    useImagePromptMuseumStore.getState().setPendingPrompt({
      promptId: 'prompt-museum-one',
      title: '编辑感产品海报',
      prompt,
    });

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const composer = await screen.findByLabelText('设计需求');
    await waitFor(() => expect(composer).toHaveValue(prompt));
    expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
    expect(useImagePromptMuseumStore.getState().pendingPrompt).toBeNull();
  });

  it('keeps historical choices fixed after a later assistant turn', async () => {
    const conversation = conversationFixture();
    conversation.messages[1] = {
      ...conversation.messages[1],
      quickReplies: ['暖色调', '冷色调'],
      generationQuote: null,
    };
    conversation.messages.push(
      {
        id: 'message-follow-up-user',
        role: 'user',
        kind: 'user',
        text: '继续调整色彩',
        quickReplies: [],
        generationQuote: null,
        turnRevision: 2,
        createdAt: '2026-07-31T10:01:00Z',
      },
      {
        id: 'message-follow-up-assistant',
        role: 'assistant',
        kind: 'choice',
        text: '请选择下一步方向。',
        quickReplies: ['增加对比度', '保持当前方案'],
        generationQuote: null,
        turnRevision: 2,
        createdAt: '2026-07-31T10:01:01Z',
      },
    );
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce(conversation);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    await screen.findByTestId('image-workspace-conversation');
    expect(screen.queryByRole('button', { name: '暖色调' })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '冷色调' })).not.toBeInTheDocument();
    expect(screen.getByRole('button', { name: '增加对比度' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '保持当前方案' })).toBeInTheDocument();
  });

  it.each(['consumed', 'expired', 'superseded'] as const)(
    'hides quote quick replies when the quote is %s',
    async (status) => {
      const conversation = conversationFixture();
      conversation.messages[1] = {
        ...conversation.messages[1],
        generationQuote: {
          ...conversation.messages[1].generationQuote!,
          status,
        },
      };
      fetchImageWorkspaceConversationMock.mockResolvedValueOnce(conversation);

      render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

      await screen.findByTestId('image-workspace-conversation');
      expect(screen.queryByRole('button', { name: '确认生成' })).not.toBeInTheDocument();
      expect(screen.queryByRole('button', { name: '继续调整' })).not.toBeInTheDocument();
      expect(screen.queryByTestId('design-quote-quote-one')).not.toBeInTheDocument();
    },
  );

  it('hides quote quick replies when the quote payload is gone', async () => {
    const conversation = conversationFixture();
    conversation.messages[1] = {
      ...conversation.messages[1],
      generationQuote: null,
    };
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce(conversation);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    await screen.findByTestId('image-workspace-conversation');
    expect(screen.queryByRole('button', { name: '确认生成' })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '继续调整' })).not.toBeInTheDocument();
  });

  it('opens a generated image at full size and saves it through Main', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const taskCard = await screen.findByTestId('design-task-task-one');
    fireEvent.click(within(taskCard).getByRole('button', { name: '查看生图任务详情' }));

    const dialog = await screen.findByRole('dialog', { name: '生成内容详情' });
    expect(dialog).toHaveTextContent('1024 × 1024');
    expect(within(dialog).getByRole('img', { name: 'AI 设计大图预览' }))
      .toHaveAttribute('src', 'http://127.0.0.1:13210/content?token=host');

    fireEvent.click(within(dialog).getByRole('button', { name: '下载原图' }));

    await waitFor(() => expect(saveImageWorkspaceAssetMock).toHaveBeenCalledWith(
      taskFixture.resultAssets[0],
    ));
  });

  it('shows video tasks as compact cards and exposes video controls in details', async () => {
    const videoAsset = {
      ...taskFixture.resultAssets[0],
      assetId: 'video-asset-one',
      mediaType: 'video' as const,
      mimeType: 'video/mp4',
      durationMilliseconds: 4_000,
    };
    const videoTask = {
      ...taskFixture,
      taskId: 'task-video-one',
      medium: 'video' as const,
      resultAssets: [videoAsset],
    };
    fetchImageWorkspaceTasksMock.mockResolvedValueOnce([videoTask]);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const taskCard = await screen.findByTestId('design-task-task-video-one');
    expect(taskCard).toHaveTextContent('视频');
    fireEvent.click(within(taskCard).getByRole('button', { name: '查看视频任务详情' }));

    const dialog = await screen.findByRole('dialog', { name: '生成内容详情' });
    expect(within(dialog).getByLabelText('AI 设计视频预览')).toBeInTheDocument();
    fireEvent.click(within(dialog).getByRole('button', { name: '下载视频' }));

    await waitFor(() => expect(saveImageWorkspaceAssetMock).toHaveBeenCalledWith(videoAsset));
  });

  it('announces the image download progress while Main is saving', async () => {
    let finishSaving: ((value: { status: 'saved' }) => void) | undefined;
    saveImageWorkspaceAssetMock.mockReturnValue(new Promise((resolve) => {
      finishSaving = resolve;
    }));
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const taskCard = await screen.findByTestId('design-task-task-one');
    fireEvent.click(within(taskCard).getByRole('button', { name: '查看生图任务详情' }));
    const dialog = await screen.findByRole('dialog', { name: '生成内容详情' });
    fireEvent.click(within(dialog).getByRole('button', { name: '下载原图' }));

    const savingButton = within(dialog).getByRole('button', { name: '正在下载生图' });
    expect(savingButton).toBeDisabled();
    expect(savingButton).toHaveAttribute('aria-busy', 'true');

    finishSaving?.({ status: 'saved' });
    await waitFor(() => expect(
      within(dialog).getByRole('button', { name: '下载原图' }),
    ).toBeEnabled());
  });

  it('keeps the task list visible in a 1404px desktop workspace', async () => {
    const previousWidth = window.innerWidth;
    Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1404 });

    try {
      render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
      await screen.findByTestId('image-workspace-conversation');

      expect(screen.getByTestId('design-task-list')).toBeInTheDocument();
      expect(screen.getByTestId('design-task-list')).toHaveClass('lg:border-l', 'lg:h-full');
    } finally {
      Object.defineProperty(window, 'innerWidth', {
        configurable: true,
        value: previousWidth,
      });
    }
  });

  it('keeps conversation and task scrolling isolated', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');

    expect(screen.getByTestId('image-canvas-page'))
      .toHaveClass('h-full', 'overflow-hidden');
    expect(screen.getByTestId('design-task-list'))
      .toHaveClass('lg:h-full', 'lg:flex', 'lg:flex-col', 'overflow-hidden');
    expect(screen.getByTestId('image-workspace-conversation'))
      .toHaveClass('overflow-y-auto', 'overscroll-contain');
    expect(screen.getByTestId('design-task-scroll'))
      .toHaveClass('lg:flex-1', 'lg:max-h-none', 'min-h-0', 'overflow-y-auto', 'overscroll-contain');
    expect(screen.getByTestId('image-workspace-conversation-fade'))
      .toHaveClass('absolute', 'top-0', 'bg-gradient-to-b');
    expect(screen.getByTestId('design-task-fade')).toBeInTheDocument();
    expect(screen.getByTestId('design-task-list').firstElementChild).not.toHaveClass('border-b');
  });

  it('explains the project model before the first Workspace is created', async () => {
    fetchImageWorkspaceMock.mockResolvedValueOnce({
      ...bootstrapFixture,
      workspaces: [],
    });

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    expect(await screen.findByText('创建第一个设计项目')).toBeInTheDocument();
    expect(screen.getByText(/持续保留与设计 Agent 的对话/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '新建设计项目' })).toBeInTheDocument();
  });

  it('sends text to the Agent with the current Turn revision instead of generating directly', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');

    fireEvent.change(screen.getByLabelText('设计需求'), {
      target: { value: '把主角换成暖色轮廓光' },
    });
    fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));

    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      1,
      '把主角换成暖色轮廓光',
      expect.stringMatching(/^turn-/),
    ));
    expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
  });

  it('sends the design message with Enter', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');
    const composer = screen.getByLabelText('设计需求');

    fireEvent.change(composer, {
      target: { value: '让画面增加一层晨雾' },
    });
    fireEvent.keyDown(composer, { key: 'Enter' });

    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      1,
      '让画面增加一层晨雾',
      expect.stringMatching(/^turn-/),
    ));
    expect(composer).toHaveValue('');
    expect(screen.getByTestId('image-workspace-composer'))
      .toHaveClass('w-[calc(100%-2rem)]', 'max-w-[46rem]');
    expect(screen.getByTestId('image-workspace-composer-surface'))
      .toHaveClass('rounded-lg', 'flex', 'min-w-0');
    expect(screen.getByRole('button', { name: '上传参考图' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '语音输入' })).toBeDisabled();
    expect(screen.queryByText('Enter 发送 · Shift+Enter 换行')).not.toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: '上传参考图' }));
    expect(screen.queryByRole('dialog', { name: '选择视频首帧' })).not.toBeInTheDocument();
    expect(screen.getByLabelText('选择参考图文件')).toBeInTheDocument();
  });

  it('uploads a reference image inline, lets the user mention it, and sends its Asset id', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');

    const file = new File(['image-bytes'], 'ocean-poster.png', { type: 'image/png' });
    fireEvent.change(screen.getByLabelText('选择参考图文件'), {
      target: { files: [file] },
    });

    await waitFor(() => expect(uploadImageWorkspaceAssetMock)
      .toHaveBeenCalledWith('workspace-cloud', file));
    const candidates = await screen.findByTestId('image-workspace-reference-candidates');
    expect(candidates).toHaveTextContent('ocean-poster.png');
    expect(screen.queryByRole('dialog', { name: '选择视频首帧' })).not.toBeInTheDocument();

    fireEvent.click(within(candidates).getByRole('button', { name: '选择参考图 ocean-poster.png' }));
    expect(screen.getByLabelText('设计需求')).toHaveValue('@ocean-poster.png ');

    fireEvent.change(screen.getByLabelText('设计需求'), {
      target: { value: '@ocean-poster.png 请沿用这张图的构图' },
    });
    fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));

    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      1,
      '@ocean-poster.png 请沿用这张图的构图',
      expect.stringMatching(/^turn-/),
      ['asset-uploaded'],
    ));
  });

  it('opens the inline reference palette when the user types @', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');

    const file = new File(['image-bytes'], 'character-sheet.png', { type: 'image/png' });
    fireEvent.change(screen.getByLabelText('选择参考图文件'), {
      target: { files: [file] },
    });
    await screen.findByTestId('image-workspace-reference-candidates');

    const composer = screen.getByLabelText('设计需求');
    fireEvent.change(composer, {
      target: { value: '请参考 @', selectionStart: 5, selectionEnd: 5 },
    });

    const palette = await screen.findByRole('listbox', { name: '选择参考图' });
    expect(within(palette).getByRole('option', { name: /character-sheet\.png/ })).toBeInTheDocument();
    fireEvent.click(within(palette).getByRole('option', { name: /character-sheet\.png/ }));
    expect(composer).toHaveValue('请参考 @character-sheet.png ');
  });

  it('keeps Shift+Enter for newlines and ignores Enter during IME composition', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');
    const composer = screen.getByLabelText('设计需求');

    fireEvent.change(composer, {
      target: { value: '正在输入中文设计描述' },
    });
    fireEvent.keyDown(composer, { key: 'Enter', shiftKey: true });
    fireEvent.keyDown(composer, { key: 'Enter', isComposing: true });

    expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
    expect(composer).toHaveValue('正在输入中文设计描述');
  });

  it('renders the pending user turn and assistant deltas while the command is running', async () => {
    const response = deferred<DesignConversation>();
    sendImageWorkspaceMessageMock.mockReturnValueOnce(response.promise);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');
    await waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce());

    fireEvent.change(screen.getByLabelText('设计需求'), {
      target: { value: '让画面更有海洋呼吸感' },
    });
    fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));

    expect(await screen.findByText('让画面更有海洋呼吸感')).toBeInTheDocument();
    expect(screen.getByRole('status', { name: '设计 Agent 正在回复' })).toBeInTheDocument();
    const pending = useImageWorkspaceStore.getState().pendingTurn!;
    act(() => {
      taskEventSource.emit('design.assistant.delta', {
        id: 'session-one:2',
        type: 'design.assistant.delta',
        workspaceId: 'workspace-cloud',
        conversationId: 'conversation-cloud',
        clientTurnId: pending.clientTurnId,
        turnRevision: pending.turnRevision,
        chunkIndex: 0,
        delta: '可以先增加',
      } satisfies DesignAssistantDeltaEvent);
    });

    expect(screen.getByTestId('image-workspace-conversation'))
      .toHaveTextContent('可以先增加');
    expect(screen.getByTestId('image-workspace-conversation'))
      .not.toHaveTextContent('留白和水流节奏');

    act(() => {
      taskEventSource.emit('design.assistant.delta', {
        id: 'session-one:3',
        type: 'design.assistant.delta',
        workspaceId: 'workspace-cloud',
        conversationId: 'conversation-cloud',
        clientTurnId: pending.clientTurnId,
        turnRevision: pending.turnRevision,
        chunkIndex: 1,
        delta: '留白和水流节奏。',
      } satisfies DesignAssistantDeltaEvent);
    });

    expect(screen.getByTestId('image-workspace-conversation'))
      .toHaveTextContent('可以先增加留白和水流节奏。');
    act(() => response.resolve({
      ...conversationFixture(),
      turnRevision: 2,
    }));
    await waitFor(() => expect(useImageWorkspaceStore.getState().pendingTurn).toBeNull());
  });

  it('reconciles a running Agent turn after returning to the conversation page', async () => {
    const response = deferred<DesignConversation>();
    const canonical: DesignConversation = {
      ...conversationFixture(),
      turnRevision: 2,
      phase: 'shaping',
      messages: [{
        id: 'message-latest',
        role: 'assistant',
        kind: 'reply',
        text: '已完成最新方向整理。',
        quickReplies: [],
        generationQuote: null,
        turnRevision: 2,
        createdAt: '2026-07-31T10:01:00Z',
      }],
    };
    sendImageWorkspaceMessageMock.mockReturnValueOnce(response.promise);

    const firstRender = render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');
    await waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce());
    await waitFor(() => expect(taskEventSource.onopen).not.toBeNull());
    act(() => taskEventSource.onopen?.(new Event('open')));

    fireEvent.change(screen.getByLabelText('设计需求'), {
      target: { value: '继续优化海报' },
    });
    fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
    expect(await screen.findByText('继续优化海报')).toBeInTheDocument();

    firstRender.unmount();
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce(canonical);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    expect(await screen.findByText('继续优化海报')).toBeInTheDocument();
    expect(await screen.findByText('已完成最新方向整理。')).toBeInTheDocument();
    expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce();
    expect(taskEventSource.close).not.toHaveBeenCalled();

    await act(async () => {
      response.resolve(canonical);
      await response.promise;
    });
  });

  it('does not expose confirmation quick replies when no active Quote exists', async () => {
    const workspaceWithoutQuote = conversationFixture();
    workspaceWithoutQuote.messages[1] = {
      ...workspaceWithoutQuote.messages[1],
      generationQuote: null,
    };
    fetchImageWorkspaceMock.mockResolvedValueOnce({
      ...bootstrapFixture,
      capabilities: {
        ...bootstrapFixture.capabilities,
        generation: false,
        image: false,
        video: false,
      },
    });
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce(workspaceWithoutQuote);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('image-workspace-conversation');
    expect(screen.queryByRole('button', { name: '确认生成' })).not.toBeInTheDocument();

    fireEvent.change(screen.getByLabelText('设计需求'), {
      target: { value: '确认生成' },
    });
    fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));

    expect(await screen.findByRole('alert')).toHaveTextContent('当前没有可确认的生成报价');
    expect(screen.getByLabelText('设计需求')).toHaveValue('');
    expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
    expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
    expect(screen.getByTestId('image-workspace-composer'))
      .toHaveTextContent('当前环境仅支持设计沟通');
  });

  it('creates a generation task only through explicit Quote confirmation', async () => {
    fetchImageWorkspaceTasksMock
      .mockResolvedValueOnce([taskFixture])
      .mockResolvedValueOnce([{
        ...taskFixture,
        taskId: 'task-two',
        quoteId: 'quote-one',
        status: 'queued',
        resultAssets: [],
      }]);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('design-quote-quote-one');

    fireEvent.click(screen.getByRole('button', { name: '确认并开始生成' }));

    await waitFor(() => expect(confirmImageWorkspaceGenerationMock)
      .toHaveBeenCalledWith(
        'workspace-cloud',
        'conversation-cloud',
        1,
        'quote-one',
        expect.stringMatching(/^turn-/),
        '夜色中的机械城堡角色海报，电影感构图。',
        {
          model: 'image-model-one',
          resolution: '1024x1024',
          aspectRatio: '1:1',
          durationSeconds: null,
        },
      ));
    await waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2));
    expect(await screen.findByTestId('design-task-task-two')).toBeInTheDocument();
  });

  it('keeps the final confirmation prompt visible in the conversation', async () => {
    const confirmedConversation: DesignConversation = {
      ...conversationFixture(),
      turnRevision: 2,
      phase: 'shaping',
      messages: [{
        id: 'message-confirmed',
        role: 'assistant',
        kind: 'reply',
        text: '设计任务已确认并开始执行。',
        quickReplies: [],
        generationQuote: null,
        turnRevision: 2,
        createdAt: '2026-07-31T10:01:00Z',
      }],
    };
    confirmImageWorkspaceGenerationMock.mockResolvedValueOnce(confirmedConversation);
    fetchImageWorkspaceTasksMock
      .mockResolvedValueOnce([taskFixture])
      .mockResolvedValueOnce([{
        ...taskFixture,
        taskId: 'task-confirmed-visible',
        quoteId: 'quote-one',
        status: 'queued',
        resultAssets: [],
      }]);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('design-quote-quote-one');
    fireEvent.click(screen.getByRole('button', { name: '确认并开始生成' }));

    expect(await screen.findByText('确认生成')).toBeInTheDocument();
    expect(screen.getByText('设计任务已确认并开始执行。')).toBeInTheDocument();
  });

  it('converts an exact composer confirmation into the structured Quote action', async () => {
    fetchImageWorkspaceTasksMock
      .mockResolvedValueOnce([taskFixture])
      .mockResolvedValueOnce([{
        ...taskFixture,
        taskId: 'task-confirmed-from-composer',
        quoteId: 'quote-one',
        status: 'queued',
        resultAssets: [],
      }]);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('design-quote-quote-one');

    fireEvent.change(screen.getByLabelText('设计需求'), {
      target: { value: '确认生成' },
    });
    fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));

    await waitFor(() => expect(confirmImageWorkspaceGenerationMock)
      .toHaveBeenCalledWith(
        'workspace-cloud',
        'conversation-cloud',
        1,
        'quote-one',
        expect.stringMatching(/^turn-/),
        '夜色中的机械城堡角色海报，电影感构图。',
        {
          model: 'image-model-one',
          resolution: '1024x1024',
          aspectRatio: '1:1',
          durationSeconds: null,
        },
      ));
    expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
    expect(await screen.findByTestId('design-task-task-confirmed-from-composer'))
      .toBeInTheDocument();
  });

  it('keeps longer design questions containing confirmation words as normal chat', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('design-quote-quote-one');
    const question = '请确认这个生成图的构图是不是还需要调整';

    fireEvent.change(screen.getByLabelText('设计需求'), {
      target: { value: question },
    });
    fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));

    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      1,
      question,
      expect.stringMatching(/^turn-/),
    ));
    expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
  });

  it('opens the successful work picker when the Agent asks for a video first frame', async () => {
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
      ...conversationFixture(),
      phase: 'shaping',
      brief: {
        version: 2,
        status: 'draft',
        medium: 'video',
        summary: '让海洋公益海报动起来',
        ready: false,
        missingDecision: '请选择首帧来源',
      },
      messages: [
        ...conversationFixture().messages,
        {
          id: 'message-video-source',
          role: 'assistant',
          kind: 'choice',
          text: '请选择视频首帧来源。',
          quickReplies: ['先生成一张首帧图片（推荐）', '从作品列表选择图片'],
          generationQuote: null,
          turnRevision: 2,
          createdAt: '2026-08-06T09:00:00Z',
        },
      ],
    } satisfies DesignConversation);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));

    const picker = screen.getByRole('dialog', { name: '选择视频首帧' });
    expect(await within(picker).findByRole('img', { name: '可选作品图片' }))
      .toHaveAttribute('src', 'http://127.0.0.1:13210/content?token=host');
    expect(within(picker).getByRole('button', { name: '选择此图片' })).toBeEnabled();
    expect(within(picker).getByLabelText('上传本地图片')).toBeInTheDocument();
    expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
  });

  it('keeps the historical video picker behavior when the brief medium is not decided yet', async () => {
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
      ...conversationFixture(),
      phase: 'shaping',
      brief: {
        version: 2,
        status: 'draft',
        medium: null,
        summary: '',
        ready: false,
        missingDecision: '请选择首帧来源',
      },
      messages: [
        ...conversationFixture().messages,
        {
          id: 'message-undecided-source',
          role: 'assistant',
          kind: 'choice',
          text: '请选择视频首帧来源。',
          quickReplies: ['从作品列表选择图片'],
          generationQuote: null,
          turnRevision: 2,
          createdAt: '2026-08-12T09:00:00Z',
        },
      ],
      turnRevision: 2,
    } satisfies DesignConversation);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));
    const picker = screen.getByRole('dialog', { name: '选择视频首帧' });
    const selectButton = within(picker).getByRole('button', { name: '选择此图片' });
    await waitFor(() => expect(selectButton).toBeEnabled());
    fireEvent.click(selectButton);

    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      2,
      '使用作品图片作为视频首帧',
      expect.stringMatching(/^turn-/),
      ['asset-one'],
    ));
    expect(screen.queryByRole('dialog', { name: '选择视频首帧' }))
      .not.toBeInTheDocument();
  });

  it('opens the image reference picker and submits a successful work Asset id', async () => {
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
      ...conversationFixture(),
      phase: 'shaping',
      brief: {
        version: 2,
        status: 'draft',
        medium: 'image',
        summary: '参考海洋公益海报生成新图',
        ready: false,
        missingDecision: '请选择参考图来源',
      },
      messages: [
        ...conversationFixture().messages,
        {
          id: 'message-image-source',
          role: 'assistant',
          kind: 'choice',
          text: '请选择图生图参考图。',
          quickReplies: ['从作品列表选择图片'],
          generationQuote: null,
          turnRevision: 2,
          createdAt: '2026-08-12T09:00:00Z',
        },
      ],
      turnRevision: 2,
    } satisfies DesignConversation);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));
    const picker = screen.getByRole('dialog', { name: '选择图生图参考图' });
    expect(within(picker).getByText(/作为图生图参考/)).toBeInTheDocument();
    const selectButton = within(picker).getByRole('button', { name: '选择此图片' });
    await waitFor(() => expect(selectButton).toBeEnabled());
    fireEvent.click(selectButton);

    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      2,
      '使用作品图片作为图生图参考图',
      expect.stringMatching(/^turn-/),
      ['asset-one'],
    ));
    expect(screen.queryByRole('dialog', { name: '选择图生图参考图' }))
      .not.toBeInTheDocument();
  });

  it('uploads a local image reference and submits its Asset id to the Agent', async () => {
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
      ...conversationFixture(),
      messages: [
        ...conversationFixture().messages,
        {
          id: 'message-image-source',
          role: 'assistant',
          kind: 'choice',
          text: '请选择图生图参考图。',
          quickReplies: ['从作品列表选择图片'],
          generationQuote: null,
          turnRevision: 2,
          createdAt: '2026-08-12T09:00:00Z',
        },
      ],
      turnRevision: 2,
    } satisfies DesignConversation);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));
    const picker = screen.getByRole('dialog', { name: '选择图生图参考图' });
    const file = new File(['image-bytes'], 'reference.png', { type: 'image/png' });

    fireEvent.change(within(picker).getByLabelText('上传本地图片'), {
      target: { files: [file] },
    });

    await waitFor(() => expect(uploadImageWorkspaceAssetMock)
      .toHaveBeenCalledWith('workspace-cloud', file));
    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      2,
      '使用上传图片作为图生图参考图',
      expect.stringMatching(/^turn-/),
      ['asset-uploaded'],
    ));
    await waitFor(() => expect(
      screen.queryByRole('dialog', { name: '选择图生图参考图' }),
    ).not.toBeInTheDocument());
  });

  it('submits a successful work Asset id when it is selected as the video first frame', async () => {
    const pendingSelection = deferred<DesignConversation>();
    sendImageWorkspaceMessageMock.mockReturnValueOnce(pendingSelection.promise);
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
      ...conversationFixture(),
      brief: {
        ...conversationFixture().brief,
        medium: 'video',
      },
      turnRevision: 2,
      messages: [
        ...conversationFixture().messages,
        {
          id: 'message-video-source',
          role: 'assistant',
          kind: 'choice',
          text: '请选择视频首帧来源。',
          quickReplies: ['从作品列表选择图片'],
          generationQuote: null,
          turnRevision: 2,
          createdAt: '2026-08-06T09:00:00Z',
        },
      ],
    } satisfies DesignConversation);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));

    const selectButton = within(
      screen.getByRole('dialog', { name: '选择视频首帧' }),
    ).getByRole('button', { name: '选择此图片' });
    await waitFor(() => expect(selectButton).toBeEnabled());
    fireEvent.click(selectButton);

    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      2,
      '使用作品图片作为视频首帧',
      expect.stringMatching(/^turn-/),
      ['asset-one'],
    ));
    expect(screen.queryByRole('dialog', { name: '选择视频首帧' })).not.toBeInTheDocument();

    await act(async () => {
      pendingSelection.resolve(conversationFixture());
      await pendingSelection.promise;
    });
  });

  it('uploads a local first-frame image and submits its Asset id to the Agent', async () => {
    fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
      ...conversationFixture(),
      brief: {
        ...conversationFixture().brief,
        medium: 'video',
      },
      messages: [
        ...conversationFixture().messages,
        {
          id: 'message-video-source',
          role: 'assistant',
          kind: 'choice',
          text: '请选择视频首帧来源。',
          quickReplies: ['从作品列表选择图片'],
          generationQuote: null,
          turnRevision: 2,
          createdAt: '2026-08-06T09:00:00Z',
        },
      ],
      turnRevision: 2,
    } satisfies DesignConversation);
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: '从作品列表选择图片' }));
    const picker = screen.getByRole('dialog', { name: '选择视频首帧' });
    const file = new File(['image-bytes'], 'ocean-poster.png', { type: 'image/png' });

    fireEvent.change(within(picker).getByLabelText('上传本地图片'), {
      target: { files: [file] },
    });

    await waitFor(() => expect(uploadImageWorkspaceAssetMock)
      .toHaveBeenCalledWith('workspace-cloud', file));
    await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
      'workspace-cloud',
      'conversation-cloud',
      2,
      '使用上传的图片作为视频首帧',
      expect.stringMatching(/^turn-/),
      ['asset-uploaded'],
    ));
    await waitFor(() => expect(
      screen.queryByRole('dialog', { name: '选择视频首帧' }),
    ).not.toBeInTheDocument());
  });

  it('renders policy-blocked generation tasks with a user-facing design message', async () => {
    fetchImageWorkspaceTasksMock.mockResolvedValue([{
      ...taskFixture,
      status: 'failed',
      failureCode: 'policy_blocked',
      resultAssets: [],
    }]);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    const taskCard = await screen.findByTestId('design-task-task-one');
    expect(within(taskCard).queryByRole('alert')).not.toBeInTheDocument();
    fireEvent.click(within(taskCard).getByRole('button', { name: '查看生图任务详情' }));
    const alert = await screen.findByRole('alert');
    expect(alert).toHaveTextContent('内容可能涉及版权或安全风险，请调整设计后重试');
    expect(alert).not.toHaveTextContent('policy_blocked');
  });

  it('renders a new task pushed by the design event stream without repeated polling', async () => {
    const queuedTask = {
      ...taskFixture,
      taskId: 'task-delayed',
      status: 'queued' as const,
      resultAssets: [],
    };
    fetchImageWorkspaceTasksMock.mockResolvedValue([]);

    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByTestId('design-quote-quote-one');
    await waitFor(() => expect(openImageWorkspaceTaskEventsMock)
      .toHaveBeenCalledWith('workspace-cloud', 'conversation-cloud'));
    await waitFor(() => expect(taskEventSource.onopen).not.toBeNull());
    act(() => {
      taskEventSource.onopen?.(new Event('open'));
      taskEventSource.emit('design.generation_task.updated', {
        id: 'session-one:2',
        type: 'design.generation_task.updated',
        workspaceId: 'workspace-cloud',
        workspaceViewRevision: 2,
        generationTask: queuedTask,
      } satisfies DesignGenerationTaskUpdatedEvent);
    });

    expect(await screen.findByTestId('design-task-task-delayed'))
      .toBeInTheDocument();
    expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledOnce();
  });
});
