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 { useImageWorkspaceStore } from '@/stores/image-workspace';
import type {
  DesignAssistantDeltaEvent,
  DesignConversation,
  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 createImageWorkspaceConversationMock = vi.hoisted(() => vi.fn());
const fetchImageWorkspaceTasksMock = vi.hoisted(() => vi.fn());
const sendImageWorkspaceMessageMock = vi.hoisted(() => vi.fn());
const confirmImageWorkspaceGenerationMock = 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)
    ),
    createImageWorkspaceConversation: (...args: unknown[]) => (
      createImageWorkspaceConversationMock(...args)
    ),
    fetchImageWorkspaceTasks: (...args: unknown[]) => fetchImageWorkspaceTasksMock(...args),
    sendImageWorkspaceMessage: (...args: unknown[]) => sendImageWorkspaceMessageMock(...args),
    confirmImageWorkspaceGeneration: (...args: unknown[]) => (
      confirmImageWorkspaceGenerationMock(...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: '夜色中的机械城堡角色海报',
          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();
    fetchImageWorkspaceMock.mockResolvedValue(bootstrapFixture);
    fetchImageWorkspaceProjectMock.mockResolvedValue(workspaceFixture());
    fetchImageWorkspaceConversationMock.mockResolvedValue(conversationFixture());
    createImageWorkspaceConversationMock.mockResolvedValue(conversationFixture());
    fetchImageWorkspaceTasksMock.mockResolvedValue([taskFixture]);
    sendImageWorkspaceMessageMock.mockResolvedValue(conversationFixture());
    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 one fixed design Agent, a Quote, and the unified image/video task list', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);

    expect(await screen.findByText('云端角色设定')).toBeInTheDocument();
    expect(screen.getByTestId('image-workspace-conversation'))
      .toHaveTextContent('方向已经明确');
    expect(screen.getByText('设计 Agent')).toBeInTheDocument();
    expect(screen.getByTestId('design-quote-quote-one')).toHaveTextContent('1 设计点');
    expect(screen.getByTestId('design-task-list')).toHaveTextContent('图片与视频任务统一展示');
    expect(screen.getByTestId('design-task-task-one')).toHaveTextContent('已完成');
    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('creates and switches Conversations without hiding project generation tasks', async () => {
    const firstConversation = conversationFixture();
    const secondConversation: DesignConversation = {
      ...conversationFixture(),
      conversationId: 'conversation-two',
      title: '第二会话',
      turnRevision: 0,
      phase: 'shaping',
      brief: {
        version: 0,
        status: 'draft',
        medium: null,
        summary: '',
        ready: false,
        missingDecision: '请描述新的设计方向',
      },
      messages: [{
        id: 'message-two',
        role: 'assistant',
        kind: 'reply',
        text: '这是独立的第二会话',
        quickReplies: [],
        generationQuote: null,
        turnRevision: 0,
        createdAt: '2026-07-31T10:05:00Z',
      }],
      createdAt: '2026-07-31T10:05:00Z',
      updatedAt: '2026-07-31T10:05:00Z',
    };
    const { messages: _firstMessages, ...firstSummary } = firstConversation;
    const { messages: _secondMessages, ...secondSummary } = secondConversation;
    fetchImageWorkspaceProjectMock
      .mockResolvedValueOnce(workspaceFixture())
      .mockResolvedValue({
        ...workspaceFixture(),
        conversationCount: 2,
        conversations: [secondSummary, firstSummary],
      });
    createImageWorkspaceConversationMock.mockResolvedValue(secondConversation);
    fetchImageWorkspaceConversationMock.mockImplementation(
      async (_workspaceId: string, conversationId: string) => (
        conversationId === secondConversation.conversationId
          ? secondConversation
          : firstConversation
      ),
    );

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

    expect(await screen.findByTestId('design-task-task-one')).toHaveTextContent('已完成');
    fireEvent.click(screen.getByRole('button', { name: '新建设计会话' }));

    await waitFor(() => expect(createImageWorkspaceConversationMock)
      .toHaveBeenCalledWith('workspace-cloud', '新会话'));
    await waitFor(() => expect(screen.getByLabelText('切换设计会话'))
      .toHaveValue('conversation-two'));
    expect(screen.getByTestId('image-workspace-conversation'))
      .toHaveTextContent('这是独立的第二会话');
    expect(screen.getByTestId('design-task-task-one')).toHaveTextContent('已完成');

    fireEvent.change(screen.getByLabelText('切换设计会话'), {
      target: { value: 'conversation-cloud' },
    });

    await waitFor(() => expect(fetchImageWorkspaceConversationMock)
      .toHaveBeenLastCalledWith('workspace-cloud', 'conversation-cloud'));
    expect(screen.getByTestId('image-workspace-conversation'))
      .toHaveTextContent('方向已经明确');
    expect(screen.getByTestId('design-task-task-one')).toHaveTextContent('已完成');
  });

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

    fireEvent.click(await screen.findByRole('button', { name: '放大查看生成图片' }));

    const dialog = screen.getByRole('dialog', { name: '图片预览' });
    expect(dialog).toHaveTextContent('1024 × 1024');
    expect(screen.getByRole('img', { name: 'AI 设计大图预览' }))
      .toHaveAttribute('src', 'http://127.0.0.1:13210/content?token=host');

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

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

  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>);

    fireEvent.click(await screen.findByRole('button', { name: '下载生成图片' }));

    const savingButton = screen.getByRole('button', { name: '正在保存生成图片' });
    expect(savingButton).toBeDisabled();
    expect(savingButton).toHaveAttribute('aria-busy', 'true');

    finishSaving?.({ status: 'saved' });
    await waitFor(() => expect(
      screen.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-pane-divider')).toHaveClass('lg:block');
    } 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('image-workspace-conversation'))
      .toHaveClass('overflow-y-auto', 'overscroll-contain');
    expect(screen.getByTestId('design-task-list'))
      .toHaveClass('lg:h-full', 'overflow-hidden');
    expect(screen.getByTestId('design-task-scroll'))
      .toHaveClass('lg:h-[calc(100%-57px)]', 'overflow-y-auto', 'overscroll-contain');
  });

  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.findByText('云端角色设定');

    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.findByText('云端角色设定');
    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'))
      .toHaveTextContent('Enter 发送 · Shift+Enter 换行');
  });

  it('keeps Shift+Enter for newlines and ignores Enter during IME composition', async () => {
    render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
    await screen.findByText('云端角色设定');
    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.findByText('云端角色设定');
    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('never downgrades confirmation to normal chat 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>);
    const confirmReply = await screen.findByRole('button', { name: '确认生成' });

    fireEvent.click(confirmReply);

    expect(screen.getByRole('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-/),
      ));
    await waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2));
    expect(await screen.findByTestId('design-task-task-two')).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-/),
      ));
    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 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();
  });
});
