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 { ImageWorkspaceSidebar } from '@/components/layout/ImageWorkspaceSidebar';
import { Sidebar } from '@/components/layout/Sidebar';
import { useOpencodeStore } from '@/stores/opencode';
import { useProjectConfigStore } from '@/stores/project-config';
import { useSettingsStore } from '@/stores/settings';
import { useUpdateStore } from '@/stores/update';
import { useAuthStore } from '@/stores/auth';
import { useImageWorkspaceStore } from '@/stores/image-workspace';
import { useUserProfileStore } from '@/stores/user-profile';
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
import { createProjectConfig } from '../../shared/project-config';
import type { DesignConversationSummary, DesignWorkspace } from '../../shared/image-workspace';

const hostApiFetchMock = vi.fn();
const invokeIpcMock = vi.fn();
const navigateMock = vi.fn();
const prepareUserAvatarMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), ensureHostApiToken: vi.fn().mockResolvedValue('test-token') }));
vi.mock('@/lib/api-client', () => ({ invokeIpc: (...args: unknown[]) => invokeIpcMock(...args) }));
vi.mock('@/lib/user-avatar', async (importOriginal) => ({
  ...await importOriginal<typeof import('@/lib/user-avatar')>(),
  prepareUserAvatar: (...args: unknown[]) => prepareUserAvatarMock(...args),
}));
vi.mock('react-router-dom', async (importOriginal) => ({ ...(await importOriginal<typeof import('react-router-dom')>()), useNavigate: () => navigateMock }));

describe('Sidebar project initialization flow', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    prepareUserAvatarMock.mockReset();
    window.electron.imageWorkspaceLocalDevelopment = false;
    useSettingsStore.setState({ sidebarCollapsed: false });
    useAuthStore.setState({ initialized: true, loading: false, error: null, accessToken: null, refreshToken: null, tokenType: null, expiresAt: null, user: null });
    useUserProfileStore.setState({ profilesByUserId: {} });
    useImageWorkspaceStore.getState().reset();
    useUpdateStore.setState({
      status: 'idle',
      currentVersion: '0.9.1',
      updateInfo: null,
      progress: null,
      error: null,
      isInitialized: true,
      isInitializing: false,
      autoInstallCountdown: null,
      init: vi.fn().mockResolvedValue(undefined),
      downloadUpdate: vi.fn().mockResolvedValue(undefined),
      installUpdate: vi.fn(),
    });
    useProjectConfigStore.setState({ configsByProjectId: {}, knowledgeByProjectId: {}, errorsByProjectId: {}, loadingProjectId: null });
    useOpencodeStore.setState({ projects: [], activeProject: null, status: { state: 'stopped', port: 4096 }, sessions: [], sessionsByProjectId: {}, selectedSessionId: null, loading: false, error: null });
    invokeIpcMock.mockResolvedValue({ canceled: true, filePaths: [] });
  });

  it('requires a personal profile name after login and saves optional details', async () => {
    useAuthStore.setState({
      user: { username: 'login@example.com', userId: 'user-1', tenantId: null, deptId: null, authorities: [] },
      accessToken: 'token',
    });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
      if (path === '/api/works/billing/token-usage') return {};
      if (path === '/api/works/user/agent-profile') {
        if (init?.method === 'PUT') {
          return {
            success: true,
            profile: {
              display_name: '小明',
              age: 16,
              gender: 'undisclosed',
              share_age_with_agents: true,
              share_gender_with_agents: false,
              analysis_enabled: true,
              completed: true,
              version: 1,
              updated_at: '2026-07-12T00:00:00Z',
            },
          };
        }
        return {
          success: true,
          profile: {
            display_name: null,
            age: null,
            gender: null,
            share_age_with_agents: false,
            share_gender_with_agents: false,
            analysis_enabled: true,
            completed: false,
            version: 0,
            updated_at: null,
          },
        };
      }
      return {};
    });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    expect(await screen.findByText('首次使用前请先填写名字。个人资料与登录账号信息相互独立。')).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '关闭个人资料' })).not.toBeInTheDocument();
    fireEvent.change(screen.getByLabelText(/名字/), { target: { value: '小明' } });
    fireEvent.change(screen.getByLabelText('年龄（选填）'), { target: { value: '16' } });
    fireEvent.click(screen.getByRole('button', { name: '保存个人资料' }));
    await waitFor(() => expect(useUserProfileStore.getState().profilesByUserId['user-1']).toMatchObject({ displayName: '小明', age: 16, version: 1 }));
  });

  it('renders the synced cloud avatar in the account surfaces and profile dialog', async () => {
    useAuthStore.setState({
      user: { username: 'login@example.com', userId: 'user-avatar', tenantId: null, deptId: null, authorities: [] },
      accessToken: 'token',
    });
    hostApiFetchMock.mockImplementation(async (path: string) => {
      if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
      if (path === '/api/works/billing/token-usage') return {};
      if (path === '/api/works/user/agent-profile') {
        return {
          success: true,
          profile: {
            display_name: '头像用户',
            age: null,
            gender: null,
            avatar_url: 'https://cdn.example/avatar.webp',
            share_age_with_agents: false,
            share_gender_with_agents: false,
            analysis_enabled: true,
            completed: true,
            version: 1,
            updated_at: '2026-08-13T00:00:00Z',
          },
        };
      }
      return {};
    });

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

    const accountAvatar = await screen.findByTestId('sidebar-account-avatar');
    await waitFor(() => expect(accountAvatar.querySelector('img')).toHaveAttribute('src', 'https://cdn.example/avatar.webp'));
    fireEvent.error(accountAvatar.querySelector('img')!);
    expect(accountAvatar).toHaveTextContent('L');

    fireEvent.click(screen.getByTestId('sidebar-member-menu-trigger'));
    expect(screen.getByTestId('sidebar-account-menu-avatar').querySelector('img')).toHaveAttribute('src', 'https://cdn.example/avatar.webp');
    fireEvent.click(screen.getByRole('menuitem', { name: '个人资料' }));
    await waitFor(() => expect(hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/works/user/agent-profile')).toHaveLength(2));
    expect(await screen.findByTestId('profile-avatar-preview')).toContainElement(
      screen.getByRole('img', { name: '个人头像预览' }),
    );
    expect(screen.getByRole('img', { name: '个人头像预览' })).toHaveAttribute('src', 'https://cdn.example/avatar.webp');
  });

  it('stages a new avatar and uploads it together with the profile save', async () => {
    useAuthStore.setState({
      user: { username: 'login@example.com', userId: 'user-upload', tenantId: null, deptId: null, authorities: [] },
      accessToken: 'token',
    });
    prepareUserAvatarMock.mockResolvedValue({
      fileName: 'avatar.webp',
      mimeType: 'image/webp',
      dataBase64: 'QUJD',
      previewUrl: 'data:image/webp;base64,QUJD',
      width: 512,
      height: 512,
      bytes: 3,
    });
    let profilePutAttempts = 0;
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
      if (path === '/api/works/billing/token-usage') return {};
      if (path === '/api/works/user/agent-profile') {
        if (init?.method === 'PUT') {
          profilePutAttempts += 1;
          if (profilePutAttempts === 1) {
            return {
              success: false,
              status: 503,
              code: 'profile_storage_unavailable',
              error: 'Profile service unavailable',
            };
          }
          return {
            success: true,
            profile: {
              display_name: '头像用户',
              age: null,
              gender: null,
              avatar_url: 'https://cdn.example/uploaded.webp',
              share_age_with_agents: false,
              share_gender_with_agents: false,
              analysis_enabled: true,
              completed: true,
              version: 2,
              updated_at: '2026-08-13T00:00:00Z',
            },
          };
        }
        return {
          success: true,
          profile: {
            display_name: '头像用户',
            age: null,
            gender: null,
            avatar_url: null,
            share_age_with_agents: false,
            share_gender_with_agents: false,
            analysis_enabled: true,
            completed: true,
            version: 1,
            updated_at: '2026-08-13T00:00:00Z',
          },
        };
      }
      if (path === '/api/works/user/avatar' && init?.method === 'POST') {
        return { success: true, avatar_url: 'https://cdn.example/uploaded.webp' };
      }
      return {};
    });

    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    fireEvent.click(await screen.findByTestId('sidebar-member-menu-trigger'));
    fireEvent.click(screen.getByRole('menuitem', { name: '个人资料' }));
    const input = screen.getByTestId('profile-avatar-upload');
    fireEvent.change(input, { target: { files: [new File(['source'], 'portrait.png', { type: 'image/png' })] } });
    await waitFor(() => expect(screen.getByRole('img', { name: '个人头像预览' })).toHaveAttribute('src', 'data:image/webp;base64,QUJD'));

    expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/works/user/avatar')).toBe(false);
    fireEvent.click(screen.getByRole('button', { name: '保存个人资料' }));

    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/user/avatar', {
      method: 'POST',
      body: JSON.stringify({ fileName: 'avatar.webp', mimeType: 'image/webp', dataBase64: 'QUJD' }),
      headers: { 'X-NianCode-Access-Token': 'token' },
    }));
    await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('个人资料服务暂时不可用'));
    expect(screen.getByRole('dialog', { name: '个人资料' })).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: '保存个人资料' }));
    await waitFor(() => expect(screen.queryByRole('dialog', { name: '个人资料' })).not.toBeInTheDocument());
    expect(profilePutAttempts).toBe(2);
    expect(hostApiFetchMock.mock.calls.filter(([path, init]) => path === '/api/works/user/avatar' && init?.method === 'POST')).toHaveLength(1);
    expect(useUserProfileStore.getState().profilesByUserId['user-upload']?.avatarUrl).toBe('https://cdn.example/uploaded.webp');
  });

  it('stages avatar removal until the profile is saved', async () => {
    useAuthStore.setState({
      user: { username: 'login@example.com', userId: 'user-remove', tenantId: null, deptId: null, authorities: [] },
      accessToken: 'token',
    });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
      if (path === '/api/works/billing/token-usage') return {};
      if (path === '/api/works/user/avatar' && init?.method === 'DELETE') return { success: true, avatar_url: null };
      if (path === '/api/works/user/agent-profile') {
        if (init?.method === 'PUT') {
          return {
            success: true,
            profile: {
              display_name: '头像用户',
              age: null,
              gender: null,
              avatar_url: null,
              share_age_with_agents: false,
              share_gender_with_agents: false,
              analysis_enabled: true,
              completed: true,
              version: 2,
              updated_at: '2026-08-13T00:00:00Z',
            },
          };
        }
        return {
          success: true,
          profile: {
            display_name: '头像用户',
            age: null,
            gender: null,
            avatar_url: 'https://cdn.example/avatar.webp',
            share_age_with_agents: false,
            share_gender_with_agents: false,
            analysis_enabled: true,
            completed: true,
            version: 1,
            updated_at: '2026-08-13T00:00:00Z',
          },
        };
      }
      return {};
    });

    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    fireEvent.click(await screen.findByTestId('sidebar-member-menu-trigger'));
    fireEvent.click(screen.getByRole('menuitem', { name: '个人资料' }));
    fireEvent.click(await screen.findByTestId('profile-avatar-remove'));
    expect(screen.getByTestId('profile-avatar-preview')).not.toContainElement(screen.queryByRole('img', { name: '个人头像预览' }));
    expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/works/user/avatar')).toBe(false);

    fireEvent.click(screen.getByRole('button', { name: '保存个人资料' }));
    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/user/avatar', {
      method: 'DELETE',
      headers: { 'X-NianCode-Access-Token': 'token' },
    }));
    await waitFor(() => expect(screen.queryByRole('dialog', { name: '个人资料' })).not.toBeInTheDocument());
    expect(useUserProfileStore.getState().profilesByUserId['user-remove']?.avatarUrl).toBeNull();
  });

  it('shows create project first and removes redundant project navigation actions', async () => {
    hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    const switcher = screen.getByTestId('sidebar-module-switcher');
    expect(switcher).toHaveTextContent('编程 Code');
    expect(screen.queryByRole('img', { name: 'Makelore logo' })).not.toBeInTheDocument();
    expect(screen.queryByText('一念成光，万物可创。')).not.toBeInTheDocument();
    const createProject = screen.getByTestId('sidebar-create-project');
    expect(createProject).toHaveTextContent('新建项目');
    expect(createProject).toHaveClass('bg-transparent', 'border-0', 'shadow-none');
    expect(screen.getByTestId('sidebar')).toHaveClass('border-r', 'sidebar-glass-surface');
    expect(screen.queryByTestId('sidebar-nav-asset-square')).not.toBeInTheDocument();
    expect(screen.queryByTestId('sidebar-nav-project-config')).not.toBeInTheDocument();
    expect(screen.queryByTestId('sidebar-nav-agent-chat')).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '添加已有项目' })).not.toBeInTheDocument();
    expect(screen.queryByText('课程路径')).not.toBeInTheDocument();
    expect(screen.queryByText('伙伴广场')).not.toBeInTheDocument();
    const moduleTrigger = screen.getByTestId('sidebar-module-switcher-trigger');
    expect(moduleTrigger).toHaveAttribute('aria-label', '返回首页');
    expect(moduleTrigger).toHaveClass('bg-transparent');
    expect(moduleTrigger).toHaveClass('w-fit', 'justify-start', 'p-2');
    expect(moduleTrigger).not.toHaveClass('bg-brand-soft');
    expect(moduleTrigger).toHaveTextContent('编程 Code');
    expect(screen.queryByTestId('sidebar-module-switcher-menu')).not.toBeInTheDocument();
    fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
    expect(navigateMock).toHaveBeenCalledWith('/module-select');
    expect(screen.queryByTestId('sidebar-nav-image-canvas')).not.toBeInTheDocument();
    expect(await screen.findByText(/暂无真实项目/)).toBeInTheDocument();
  });

  it('keeps new project and personal profile as dismissible modal dialogs', async () => {
    hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);

    fireEvent.click(screen.getByTestId('sidebar-create-project'));
    const createDialog = screen.getByRole('dialog', { name: '新建项目' });
    expect(createDialog).toHaveAttribute('aria-modal', 'true');
    act(() => fireEvent.keyDown(document, { key: 'Escape' }));
    await waitFor(() => expect(screen.queryByRole('dialog', { name: '新建项目' })).not.toBeInTheDocument());

    fireEvent.click(screen.getByTestId('sidebar-member-menu-trigger'));
    fireEvent.click(screen.getByRole('menuitem', { name: '个人资料' }));
    const profileDialog = screen.getByRole('dialog', { name: '个人资料' });
    expect(profileDialog).toHaveAttribute('aria-modal', 'true');
    act(() => fireEvent.keyDown(document, { key: 'Escape' }));
    await waitFor(() => expect(screen.queryByRole('dialog', { name: '个人资料' })).not.toBeInTheDocument());
  });

  it('keeps the expanded account menu aligned and scrollable within the sidebar', async () => {
    hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);

    const account = screen.getByTestId('sidebar-member-menu-trigger');
    expect(account).toHaveClass('min-h-9', 'px-2', 'py-1');
    fireEvent.click(account);

    const menu = screen.getByTestId('sidebar-account-menu');
    expect(menu).toHaveAttribute('data-state', 'open');
    expect(menu).toHaveClass('absolute', 'left-0', 'w-full');
    expect(menu.querySelector('.codex-disclosure-inner')).toHaveClass('max-h-[calc(100vh-7rem)]', 'overflow-y-auto');
    expect(menu).not.toHaveClass('fixed');

    const usageItem = screen.getByTestId('sidebar-account-usage-menuitem');
    expect(usageItem).toHaveClass('min-h-8');
    expect(screen.getByTestId('sidebar-account-usage-drawer')).toHaveAttribute('data-state', 'closed');

    fireEvent.click(usageItem);
    const usageDrawer = screen.getByTestId('sidebar-account-usage-drawer');
    expect(usageDrawer).toHaveTextContent('订阅等级');
    expect(usageDrawer).toHaveTextContent('5 小时');
    expect(usageDrawer).toHaveTextContent('1 周');
  });

  it('places the module switcher in the sidebar header above project actions', async () => {
    hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);

    const switcher = screen.getByTestId('sidebar-module-switcher');
    const createProject = screen.getByTestId('sidebar-create-project');
    expect(Boolean(switcher.compareDocumentPosition(createProject) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
  });

  it('fully hides the collapsed sidebar and exposes a full hover preview', () => {
    useSettingsStore.setState({ sidebarCollapsed: true });
    const onSidebarPeekChange = vi.fn();
    const { rerender } = render(
      <MemoryRouter>
        <Sidebar sidebarPeekOpen={false} onSidebarPeekChange={onSidebarPeekChange} />
      </MemoryRouter>,
    );

    const sidebar = screen.getByTestId('sidebar');
    expect(sidebar).toHaveClass('w-0', 'pointer-events-none', 'opacity-0', 'fixed', 'inset-y-0', 'left-0', 'z-[350]');
    expect(sidebar).not.toHaveClass('sidebar-peek-surface', 'pt-10');

    rerender(
      <MemoryRouter>
        <Sidebar sidebarPeekOpen onSidebarPeekChange={onSidebarPeekChange} />
      </MemoryRouter>,
    );

    expect(sidebar).toHaveClass('w-64', 'fixed', 'inset-y-0', 'z-[350]', 'sidebar-peek-surface', 'pt-10', 'shadow-float');
    expect(screen.getByTestId('sidebar-create-project')).toHaveTextContent('新建项目');
    fireEvent.pointerEnter(sidebar);
    expect(onSidebarPeekChange).toHaveBeenCalledWith(true, 'sidebar');
    fireEvent.pointerLeave(sidebar);
    expect(onSidebarPeekChange).toHaveBeenCalledWith(false, 'sidebar');
  });

  it('keeps a clicked-open sidebar on the translucent blurred material', () => {
    useSettingsStore.setState({ sidebarCollapsed: false });
    render(
      <MemoryRouter>
        <Sidebar />
      </MemoryRouter>,
    );

    const sidebar = screen.getByTestId('sidebar');
    expect(sidebar).toHaveClass('w-64', 'sidebar-glass-surface');
    expect(sidebar).not.toHaveClass('fixed', 'sidebar-peek-surface', 'pt-10');
  });

  it('shows an available update beside the lower-left account control', async () => {
    hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);

    const account = screen.getByTestId('sidebar-member-menu-trigger');
    expect(screen.queryByTestId('sidebar-update-button')).not.toBeInTheDocument();

    act(() => {
      useUpdateStore.setState({
        status: 'available',
        updateInfo: { version: '0.9.2' },
      });
    });

    const update = await screen.findByTestId('sidebar-update-button');
    expect(account.parentElement).toBe(update.parentElement);
    expect(Boolean(account.compareDocumentPosition(update) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
    expect(update).toHaveAccessibleName('下载新版本 0.9.2');
  });

  it('defaults to a publishable mini-game project and opens configuration', async () => {
    const project = { id: 'prj_game', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '2026-07-11T00:00:00.000Z', updatedAt: '2026-07-11T00:00:00.000Z', lastOpenedAt: '2026-07-11T00:00:00.000Z' };
    const config = createProjectConfig(undefined, 'mini_game');
    invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo/星星收集器'] });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
      if (path === '/api/opencode/projects/create') return { success: true, project, projects: [project], activeProject: null, config };
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
      if (path === '/api/opencode/projects/active' && init?.method === 'POST') return { success: true, project, projects: [project], activeProject: project };
      throw new Error(`Unexpected path ${path}`);
    });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: '新建项目' }));
    expect(screen.getByRole('radio', { name: '小游戏' })).toBeChecked();
    expect(screen.getByRole('radio', { name: '小程序' })).not.toBeChecked();
    expect(screen.getByRole('radio', { name: '自定义项目' })).not.toBeChecked();
    expect(screen.getByRole('radio', { name: '直接使用所选文件夹（默认）' })).toBeChecked();
    expect(screen.queryByLabelText('项目名称')).not.toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: '选择路径' }));
    await waitFor(() => expect(screen.getByLabelText('项目路径')).toHaveValue('D:/repo/星星收集器'));
    expect(screen.getByText('将直接接入此文件夹，项目名称使用“星星收集器”，不会重命名目录。')).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: '确认创建' }));
    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', { method: 'POST', body: JSON.stringify({ projectPath: 'D:/repo/星星收集器', projectType: 'mini_game' }) }));
    await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/project-config'));
  });

  it('keeps creating a named child folder when that directory mode is selected', async () => {
    const project = { id: 'prj_child', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '', updatedAt: '', lastOpenedAt: '' };
    const config = createProjectConfig(undefined, 'mini_program');
    invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo'] });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
      if (path === '/api/opencode/projects/create') return { success: true, project, projects: [project], activeProject: null, config };
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
      if (path === '/api/opencode/projects/active' && init?.method === 'POST') return { success: true, project, projects: [project], activeProject: project };
      throw new Error(`Unexpected path ${path}`);
    });

    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: '新建项目' }));
    fireEvent.click(screen.getByRole('radio', { name: '小程序' }));
    fireEvent.click(screen.getByRole('radio', { name: '在所选位置新建下级文件夹' }));
    fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '星星收集器' } });
    fireEvent.click(screen.getByRole('button', { name: '选择路径' }));
    await waitFor(() => expect(screen.getByLabelText('项目路径')).toHaveValue('D:/repo'));
    fireEvent.click(screen.getByRole('button', { name: '确认创建' }));

    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', {
      method: 'POST',
      body: JSON.stringify({
        parentPath: 'D:/repo',
        projectName: '星星收集器',
        projectType: 'mini_program',
      }),
    }));
  });

  it('opens initialized projects directly in Agent collaboration', async () => {
    const project = { id: 'prj_ready', path: 'D:/repo/ready', name: 'ready', createdAt: '', updatedAt: '', lastOpenedAt: '' };
    const config = { ...createProjectConfig(), initialized: true };
    hostApiFetchMock.mockImplementation(async (path: string) => {
      if (path === '/api/opencode/projects') return { projects: [project], activeProject: null };
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
      if (path === '/api/opencode/projects/active') return { success: true, project, projects: [project], activeProject: project };
      throw new Error(`Unexpected path ${path}`);
    });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: `进入项目 ${project.name}` }));
    await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/opencode-chat'));
  });

  it('removes a project card when its local project configuration is missing', async () => {
    const project = { id: 'prj_missing', path: 'D:/repo/missing', name: 'missing', createdAt: '', updatedAt: '', lastOpenedAt: '' };
    const staleConfig = createProjectConfig();
    useProjectConfigStore.setState({ configsByProjectId: { [project.id]: staleConfig }, knowledgeByProjectId: {}, errorsByProjectId: {}, loadingProjectId: null });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/opencode/projects') return { projects: [project], activeProject: null };
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'missing', knowledgeFiles: [] };
      if (path === '/api/opencode/projects/remove' && init?.method === 'POST') return { success: true, removedProjectId: project.id, projects: [], activeProject: null };
      throw new Error(`Unexpected path ${path}`);
    });

    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: `进入项目 ${project.name}` }));
    const projectConfigError = await screen.findByTestId('project-config-error');
    expect(projectConfigError).toBeInTheDocument();
    expect(screen.getByTestId('project-config-error-overlay')).toHaveClass('fixed', 'inset-0', 'backdrop-blur-md');
    expect(projectConfigError.closest('[data-testid="sidebar"]')).toBeNull();
    fireEvent.click(screen.getByRole('button', { name: '移除项目卡片' }));
    const removeDialog = screen.getByRole('dialog', { name: `移除项目“${project.name}”？` });
    expect(removeDialog).toBeInTheDocument();
    expect(removeDialog).toHaveAttribute('aria-modal', 'true');
    expect(removeDialog.closest('[data-testid="sidebar"]')).toBeNull();
    fireEvent.click(screen.getByRole('button', { name: '确认移除' }));

    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/remove', {
      method: 'POST',
      body: JSON.stringify({ projectId: project.id }),
    }));
    await waitFor(() => expect(screen.queryByTestId(`sidebar-course-project-${project.id}`)).not.toBeInTheDocument());
    expect(useProjectConfigStore.getState().configsByProjectId[project.id]).toBeUndefined();
  });

  it('uses the whole project card as the enter action', async () => {
    const project = { id: 'prj_config', path: 'D:/repo/config', name: 'config', createdAt: '', updatedAt: '', lastOpenedAt: '' };
    const config = {
      ...createProjectConfig(),
      initialized: true,
      agents: ['小策', '小设', '小码', '小发'].map((name, index) => ({
        id: `manual-agent-${index}`,
        name,
        builtIn: false,
        model: null,
        skills: [],
        responsibility: '',
        avatarId: `avatar-${16 - index}`,
      })),
    };
    hostApiFetchMock.mockImplementation(async (path: string) => {
      if (path === '/api/opencode/projects') return { projects: [project], activeProject: null };
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
      if (path === '/api/opencode/projects/active') return { success: true, project, projects: [project], activeProject: project };
      throw new Error(`Unexpected path ${path}`);
    });
    render(<MemoryRouter><Sidebar /></MemoryRouter>);
    const card = await screen.findByTestId(`sidebar-course-project-${project.id}`);
    expect(card).toHaveTextContent(project.name);
    expect(card).not.toHaveTextContent('普通开发项目');
    expect(card).not.toHaveTextContent(project.path);
    expect(card).not.toHaveTextContent('已初始化');
    expect(card).not.toHaveTextContent('最后活跃');
    expect(card).not.toHaveTextContent('移除项目');
    expect(screen.getByLabelText('项目 Agent').children).toHaveLength(4);
    expect(screen.getByTitle('小策').querySelector('img')).toHaveAttribute('src', getAgentAvatarSrc('avatar-16'));
    expect(screen.getByTitle('小设').querySelector('img')).toHaveAttribute('src', getAgentAvatarSrc('avatar-15'));
    expect(screen.getByRole('button', { name: `进入项目 ${project.name}` })).toBe(card);
    expect(card.querySelectorAll('button')).toHaveLength(0);
    expect(screen.queryByRole('button', { name: `配置项目 ${project.name}` })).not.toBeInTheDocument();
  });

  it('switches the sidebar to titled Design Workspaces inside the AI painting module', async () => {
    useAuthStore.setState({
      accessToken: 'cloud-token',
      expiresAt: Date.now() + 60_000,
    });
    const summary = {
      workspaceId: 'cloud-project',
      title: '云端概念设计',
      viewRevision: 0,
      conversationCount: 1,
      phase: 'shaping',
      updatedAt: '2026-07-31T10:00:00Z',
    };
    const conversation = {
      conversationId: 'cloud-conversation',
      workspaceId: 'cloud-project',
      title: '主会话',
      latestMessagePreview: '第一条历史消息',
      turnRevision: 0,
      phase: 'shaping',
      brief: {
        version: 0,
        status: 'draft',
        medium: null,
        summary: '正在建立作品的视觉方向',
        ready: false,
        missingDecision: '作品形式',
      },
      createdAt: '2026-07-31T10:00:00Z',
      updatedAt: '2026-07-31T10:00:00Z',
      messages: [],
    };
    const { messages: _messages, ...conversationSummary } = conversation;
    const secondConversation = {
      ...conversation,
      conversationId: 'cloud-conversation-two',
      title: '第二会话',
      latestMessagePreview: '第二条历史消息',
      messages: [],
      createdAt: '2026-07-31T10:05:00Z',
      updatedAt: '2026-07-31T10:05:00Z',
    };
    const { messages: _secondMessages, ...secondConversationSummary } = secondConversation;
    let hasSecondConversation = false;
    const bootstrap = {
      capabilities: {
        conversation: true,
        generation: true,
        image: true,
        video: true,
      },
      workspaces: [summary],
    };
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/works/image-workspace') {
        return { success: true, data: bootstrap };
      }
      if (path === '/api/works/image-workspace/workspaces/cloud-project' && !init?.method) {
        return {
          success: true,
          data: {
            ...summary,
            conversationCount: hasSecondConversation ? 2 : 1,
            conversations: hasSecondConversation
              ? [secondConversationSummary, conversationSummary]
              : [conversationSummary],
          },
        };
      }
      if (path === '/api/works/image-workspace/workspaces/cloud-project/conversations/cloud-conversation') {
        return { success: true, data: conversation };
      }
      if (path === '/api/works/image-workspace/workspaces/cloud-project-two/conversations/cloud-conversation-two') {
        return {
          success: true,
          data: {
            ...conversation,
            workspaceId: 'cloud-project-two',
            conversationId: 'cloud-conversation-two',
          },
        };
      }
      if (path === '/api/works/image-workspace/workspaces/cloud-project/tasks') {
        return { success: true, data: [] };
      }
      if (path === '/api/works/image-workspace/workspaces/cloud-project/conversations'
        && init?.method === 'POST') {
        hasSecondConversation = true;
        return { success: true, data: secondConversation };
      }
      if (path === '/api/works/image-workspace/workspaces' && init?.method === 'POST') {
        return {
          success: true,
          data: {
            ...summary,
            workspaceId: 'cloud-project-two',
            title: '角色设定',
            conversations: [{
              ...conversationSummary,
              workspaceId: 'cloud-project-two',
              conversationId: 'cloud-conversation-two',
            }],
          },
        };
      }
      throw new Error(`Unexpected path ${path}`);
    });

    render(<MemoryRouter initialEntries={['/image-canvas']}><Sidebar /></MemoryRouter>);

    const projectCard = await screen.findByTestId('sidebar-image-project-cloud-project');
    expect(projectCard).toHaveTextContent('云端概念设计');
    expect(projectCard).not.toHaveTextContent('1 个会话');
    expect(projectCard).not.toHaveTextContent('设计会话');
    expect(screen.getByRole('button', { name: '收起 云端概念设计 的会话' })).toBeVisible();
    expect(screen.getByRole('button', { name: '重命名 云端概念设计' })).toBeVisible();
    const projectHeader = screen.getByTestId('sidebar-image-project-header-cloud-project');
    const projectActions = [
      screen.getByRole('button', { name: '收起 云端概念设计 的会话' }),
      screen.getByRole('button', { name: '重命名 云端概念设计' }),
      within(projectHeader).getByRole('button', { name: '新建设计会话' }),
    ];
    projectActions.forEach((action) => {
      expect(action).toHaveClass('opacity-0', 'pointer-events-none', 'group-hover/project:opacity-100', 'focus-visible:opacity-100');
    });
    expect(projectHeader).toHaveClass('items-center');
    expect(projectHeader).not.toHaveClass('justify-center');
    expect(projectHeader).toHaveClass('min-h-9');
    expect(within(screen.getByTestId('sidebar-image-conversations-cloud-project')).queryByRole('button', { name: '新建设计会话' }))
      .not.toBeInTheDocument();
    expect(screen.getByTestId('sidebar-image-project-toggle-icon-cloud-project')).toHaveClass('rotate-180');
    expect(screen.getByTestId('sidebar-image-conversations-cloud-project'))
      .not.toHaveClass('border-t');
    expect(screen.getByTestId('sidebar-module-switcher-trigger')).toBeVisible();
    const createImageProject = screen.getByTestId('sidebar-create-image-project');
    expect(createImageProject).toBeEnabled();
    expect(createImageProject).toHaveClass('bg-transparent', 'border-0', 'shadow-none');
    expect(screen.queryByTestId('sidebar-create-project')).not.toBeInTheDocument();
    expect(screen.queryByText('项目路径')).not.toBeInTheDocument();
    expect(screen.queryByText('项目模板')).not.toBeInTheDocument();
    expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/opencode/projects')).toBe(false);
    expect(screen.queryByText(/添加 Agent/)).not.toBeInTheDocument();
    expect(await screen.findByTestId('sidebar-image-conversation-cloud-conversation'))
      .toHaveAttribute('aria-current', 'page');
    const firstConversation = screen.getByTestId('sidebar-image-conversation-cloud-conversation');
    expect(firstConversation).toHaveTextContent('第一条历史消息');
    expect(firstConversation).not.toHaveTextContent('主会话');
    expect(screen.getByRole('button', { name: '新建设计会话' })).toBeEnabled();

    fireEvent.click(screen.getByRole('button', { name: '新建设计会话' }));
    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith(
      '/api/works/image-workspace/workspaces/cloud-project/conversations',
      expect.objectContaining({ method: 'POST' }),
    ));
    expect(await screen.findByTestId('sidebar-image-conversation-cloud-conversation-two'))
      .toHaveAttribute('aria-current', 'page');
    const secondConversationButton = screen.getByTestId('sidebar-image-conversation-cloud-conversation-two');
    expect(secondConversationButton).toHaveTextContent('第二条历史消息');
    expect(secondConversationButton).not.toHaveTextContent('第二会话');

    fireEvent.click(screen.getByTestId('sidebar-image-conversation-cloud-conversation'));
    await waitFor(() => expect(screen.getByTestId('sidebar-image-conversation-cloud-conversation'))
      .toHaveAttribute('aria-current', 'page'));

    fireEvent.click(screen.getByTestId('sidebar-create-image-project'));
    expect(screen.getByRole('dialog', { name: '新建设计项目' })).toBeInTheDocument();
    expect(screen.getByLabelText('项目名称')).toBeInTheDocument();
    expect(screen.queryByLabelText('项目路径')).not.toBeInTheDocument();
    expect(screen.queryByText('项目模板')).not.toBeInTheDocument();
    fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '角色设定' } });
    fireEvent.click(screen.getByRole('button', { name: '创建项目' }));

    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith(
      '/api/works/image-workspace/workspaces',
      expect.objectContaining({ method: 'POST' }),
    ));
    const createCall = hostApiFetchMock.mock.calls.find(
      ([path, init]) => path === '/api/works/image-workspace/workspaces'
        && (init as RequestInit | undefined)?.method === 'POST',
    );
    expect(JSON.parse(String((createCall?.[1] as RequestInit).body))).toMatchObject({
      title: '角色设定',
      clientWorkspaceId: expect.stringMatching(/^workspace-/),
    });
  });

  it('keeps identical project copy when Main injects the local development adapter', async () => {
    window.electron.imageWorkspaceLocalDevelopment = true;
    const summary = {
      workspaceId: 'local-project',
      title: '概念设计',
      viewRevision: 0,
      conversationCount: 1,
      phase: 'shaping',
      updatedAt: '2026-07-31T10:00:00Z',
    };
    const conversation = {
      conversationId: 'local-conversation',
      workspaceId: 'local-project',
      title: '主会话',
      turnRevision: 0,
      phase: 'shaping',
      brief: {
        version: 0,
        status: 'draft',
        medium: null,
        summary: '正在建立作品的视觉方向',
        ready: false,
        missingDecision: '作品形式',
      },
      createdAt: '2026-07-31T10:00:00Z',
      updatedAt: '2026-07-31T10:00:00Z',
      messages: [],
    };
    const { messages: _messages, ...conversationSummary } = conversation;
    let bootstrap = {
      capabilities: {
        conversation: true,
        generation: true,
        image: true,
        video: true,
      },
      workspaces: [summary],
    };
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === '/api/works/image-workspace' && !init?.method) {
        return { success: true, data: bootstrap };
      }
      if (path === '/api/works/image-workspace/workspaces/local-project' && !init?.method) {
        return { success: true, data: { ...summary, conversations: [conversationSummary] } };
      }
      if (path === '/api/works/image-workspace/workspaces/local-project/conversations/local-conversation') {
        return { success: true, data: conversation };
      }
      if (path === '/api/works/image-workspace/workspaces/local-project/tasks') {
        return { success: true, data: [] };
      }
      if (path === '/api/works/image-workspace/workspaces' && init?.method === 'POST') {
        const created = {
          ...summary,
          workspaceId: 'local-project-two',
          title: '角色设计',
          conversations: [{
            ...conversationSummary,
            workspaceId: 'local-project-two',
            conversationId: 'local-conversation-two',
          }],
        };
        bootstrap = { ...bootstrap, workspaces: [created, ...bootstrap.workspaces] };
        return { success: true, data: created };
      }
      if (path === '/api/works/image-workspace/workspaces/local-project-two/conversations/local-conversation-two') {
        return {
          success: true,
          data: {
            ...conversation,
            workspaceId: 'local-project-two',
            conversationId: 'local-conversation-two',
          },
        };
      }
      throw new Error(`Unexpected path ${path}`);
    });

    const { container } = render(<MemoryRouter initialEntries={['/image-canvas']}><Sidebar /></MemoryRouter>);

    expect(await screen.findByTestId('sidebar-image-project-local-project')).toHaveTextContent('概念设计');
    expect(screen.getByTestId('sidebar-create-image-project')).toHaveTextContent('新建设计项目');
    expect(screen.getByTestId('sidebar-image-projects')).toBeInTheDocument();
    expect(container).not.toHaveTextContent('本地开发');
    expect(container).not.toHaveTextContent('本地项目');
    expect(screen.queryByTestId('sidebar-reset-local-image-workspace')).not.toBeInTheDocument();
    expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/image-workspace', {});

    fireEvent.click(screen.getByTestId('sidebar-create-image-project'));
    expect(screen.getByRole('dialog', { name: '新建设计项目' })).toBeInTheDocument();
    fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '角色设计' } });
    fireEvent.click(screen.getByRole('button', { name: '创建项目' }));
    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith(
      '/api/works/image-workspace/workspaces',
      expect.objectContaining({ method: 'POST' }),
    ));
    expect(await screen.findByText('角色设计')).toBeInTheDocument();
  });

  it('keeps older design conversations behind a more-conversations disclosure', () => {
    const conversations: DesignConversationSummary[] = Array.from({ length: 7 }, (_, index) => ({
      conversationId: `history-conversation-${index + 1}`,
      workspaceId: 'history-project',
      title: `历史会话 ${index + 1}`,
      latestMessagePreview: `历史消息 ${index + 1}`,
      turnRevision: index,
      phase: 'shaping',
      brief: {
        version: index,
        status: 'draft',
        medium: null,
        summary: '正在建立作品的视觉方向',
        ready: false,
        missingDecision: '作品形式',
      },
      createdAt: `2026-07-31T10:${String(6 - index).padStart(2, '0')}:00Z`,
      updatedAt: `2026-07-31T10:${String(6 - index).padStart(2, '0')}:00Z`,
    }));
    const workspace: DesignWorkspace = {
      workspaceId: 'history-project',
      title: '历史设计项目',
      viewRevision: 7,
      conversationCount: conversations.length,
      phase: 'shaping',
      updatedAt: '2026-07-31T10:06:00Z',
      conversations,
    };
    useImageWorkspaceStore.setState({
      status: 'ready',
      bootstrap: {
        capabilities: { conversation: true, generation: true, image: true, video: true },
        workspaces: [{
          workspaceId: workspace.workspaceId,
          title: workspace.title,
          viewRevision: workspace.viewRevision,
          conversationCount: workspace.conversationCount,
          phase: workspace.phase,
          updatedAt: workspace.updatedAt,
        }],
      },
      activeWorkspaceId: workspace.workspaceId,
      activeConversationId: conversations[0].conversationId,
      workspace,
    });

    render(<MemoryRouter><ImageWorkspaceSidebar sidebarCollapsed={false} /></MemoryRouter>);

    expect(screen.getByTestId('sidebar-image-workspace')).toHaveClass('font-sans');
    expect(screen.getByTestId('sidebar-image-conversation-history-conversation-1'))
      .toHaveTextContent('历史消息 1');
    expect(screen.getByTestId('sidebar-image-more-conversations-history-project'))
      .toHaveAttribute('data-state', 'closed');
    fireEvent.click(screen.getByRole('button', { name: '更多会话（2）' }));
    expect(screen.getByTestId('sidebar-image-more-conversations-history-project'))
      .toHaveAttribute('data-state', 'open');
    expect(screen.getByTestId('sidebar-image-conversation-history-conversation-7'))
      .toHaveTextContent('历史消息 7');
  });

});
