import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { StrictMode, type ComponentProps } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentConversationSidebar } from '@/components/opencode/AgentConversationSidebar';
import type { ConfiguredModelOption } from '@/lib/model-options';
import { useSettingsStore } from '@/stores/settings';
import type { ProjectAgentConfig } from '../../shared/project-config';
import type { ProjectSessionMetadata } from '../../shared/project-conversations';
import type { OpencodeSession } from '@/types/opencode';

const modelOption: ConfiguredModelOption = {
  modelRef: 'openai/gpt-4o-mini',
  label: 'gpt-4o-mini',
  runtimeProviderKey: 'openai',
  accountId: 'account-openai',
  capability: 'text',
};

function agent(id: string, name: string, overrides: Partial<ProjectAgentConfig> = {}): ProjectAgentConfig {
  return {
    id,
    avatarId: 'avatar-01',
    roleName: '项目伙伴',
    name,
    builtIn: false,
    enabled: true,
    model: modelOption.modelRef,
    skillIds: [],
    responsibility: { mission: `${name}的职责`, owns: [], boundaries: [], collaborators: [], principles: [] },
    prompt: '',
    archivedAt: null,
    pinned: false,
    ...overrides,
  };
}

function session(id: string, title: string): OpencodeSession {
  return { id, title, updatedAt: '2026-07-30T00:02:00.000Z' } as OpencodeSession;
}

function metadata(sessionId: string, agentId: string, overrides: Partial<ProjectSessionMetadata> = {}): ProjectSessionMetadata {
  return {
    sessionId,
    agentId,
    archivedAt: null,
    unreadCount: 0,
    createdAt: '2026-07-30T00:01:00.000Z',
    updatedAt: '2026-07-30T00:02:00.000Z',
    ...overrides,
  };
}

function renderSidebar(
  overrides: Partial<ComponentProps<typeof AgentConversationSidebar>> = {},
  options: { strictMode?: boolean } = {},
) {
  const callbacks = {
    onSelectAgent: vi.fn(),
    onSelectSession: vi.fn(),
    onNewSession: vi.fn(),
    onCreateAgent: vi.fn().mockResolvedValue(undefined),
    onArchiveAgent: vi.fn().mockResolvedValue(undefined),
    onRestoreAgent: vi.fn().mockResolvedValue(undefined),
    onDeleteAgent: vi.fn().mockResolvedValue(undefined),
    onArchiveSession: vi.fn().mockResolvedValue(undefined),
    onRestoreSession: vi.fn().mockResolvedValue(undefined),
    onDeleteSession: vi.fn().mockResolvedValue(undefined),
    onTogglePinAgent: vi.fn().mockResolvedValue(undefined),
    onOpenProjectSettings: vi.fn(),
  };
  const sidebar = (
    <AgentConversationSidebar
      agents={[agent('agent-a', '小明'), agent('agent-b', '小红', { pinned: true })]}
      sessions={[session('ses-a', '第一次聊天')]}
      sessionMessagesBySessionId={{}}
      conversationSessions={[metadata('ses-a', 'agent-a', { unreadCount: 2 })]}
      selectedAgentId={null}
      selectedSessionId={null}
      sessionStatuses={{}}
      pendingQuestionCountBySession={{}}
      pendingPermissionCountBySession={{}}
      startingAgentId={null}
      modelOptions={[modelOption]}
      {...callbacks}
      {...overrides}
    />
  );
  render(options.strictMode ? <StrictMode>{sidebar}</StrictMode> : sidebar);
  return callbacks;
}

describe('AgentConversationSidebar', () => {
  beforeEach(() => {
    useSettingsStore.setState({ sidebarCollapsed: false });
  });

  afterEach(() => {
    cleanup();
    useSettingsStore.setState({ sidebarCollapsed: false });
  });

  it('uses a flush square sidebar surface without an outer card frame', () => {
    renderSidebar();

    const sidebar = screen.getByTestId('agent-conversation-sidebar');
    expect(sidebar).toHaveClass('no-drag', 'pointer-events-auto', 'absolute', 'inset-y-0', 'left-0', 'w-[256px]', 'min-w-[256px]', 'max-w-[256px]', 'z-[60]', 'rounded-none', 'border-r');
    expect(sidebar).not.toHaveClass('rounded-xl', 'shadow-soft');
    expect(screen.getByTestId('agent-conversation-sidebar-content')).toHaveClass('p-0');
    expect(screen.getByTestId('agent-conversation-project-context')).toHaveTextContent('Makelore 工作台');
  });

  it('keeps the conversation actions in the conversation list title rail', () => {
    renderSidebar();

    const sidebar = screen.getByTestId('agent-conversation-sidebar');
    const header = screen.getByTestId('agent-conversation-sidebar-header');
    expect(sidebar).not.toContainElement(header);
    const actionPortal = screen.getByTestId('agent-conversation-sidebar-titlebar-portal');
    expect(actionPortal).toContainElement(header);
    expect(actionPortal).toHaveClass('border-r');
    expect(actionPortal).toHaveStyle({ left: '256px' });
    expect(header).toHaveClass('w-full', 'shrink-0', 'items-center');
    expect(header).not.toHaveClass('py-1');
    expect(header).toHaveClass('pointer-events-none');
    expect(header).not.toHaveClass('drag-region');
    expect(header.firstElementChild).toHaveClass('no-drag', 'pointer-events-auto');
    expect(screen.getByTestId('agent-conversation-dialog-context-portal')).toHaveStyle({ left: '512px' });
    expect(screen.getByTestId('agent-conversation-dialog-context')).toContainElement(screen.getByTestId('agent-conversation-project-context'));
    expect(screen.queryByRole('button', { name: '真机预览' })).not.toBeInTheDocument();
    expect(screen.getByRole('button', { name: '项目设置' })).toHaveClass('no-drag');
    const createButton = screen.getByRole('button', { name: '创建伙伴' });
    expect(createButton).toHaveClass('no-drag', 'text-brand');
    expect(createButton.querySelector('svg')).toHaveClass('text-brand');
    expect(createButton).toHaveAttribute('aria-expanded', 'false');

    fireEvent.click(createButton);
    expect(createButton).toHaveClass('bg-brand', 'text-primary-foreground');
    expect(createButton.querySelector('svg')).toHaveClass('text-primary-foreground');
    expect(createButton).toHaveAttribute('aria-expanded', 'true');
  });

  it('keeps both title rails aligned when the main sidebar is collapsed', () => {
    useSettingsStore.setState({ sidebarCollapsed: true });
    renderSidebar();

    const sidebar = screen.getByTestId('agent-conversation-sidebar');
    const header = screen.getByTestId('agent-conversation-sidebar-header');
    const actionPortal = screen.getByTestId('agent-conversation-sidebar-titlebar-portal');
    const contextPortal = screen.getByTestId('agent-conversation-dialog-context-portal');
    expect(sidebar).not.toContainElement(header);
    expect(actionPortal).toContainElement(header);
    expect(actionPortal).toHaveClass('pointer-events-none');
    expect(actionPortal).toHaveStyle({ left: '0px' });
    expect(contextPortal).toHaveStyle({ left: '256px' });
  });

  it('keeps the agent creation dialog above the conversation sidebar', () => {
    renderSidebar();

    fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));

    expect(screen.getByRole('dialog')).toHaveClass('z-[110]');
  });

  it('keeps the fixed actions mounted through React StrictMode effect replay', () => {
    renderSidebar({}, { strictMode: true });

    expect(screen.getByRole('button', { name: '项目设置' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '创建伙伴' })).toBeInTheDocument();
  });

  it('opens project settings from the conversation header', () => {
    const callbacks = renderSidebar();

    fireEvent.click(screen.getByRole('button', { name: '项目设置' }));
    expect(callbacks.onOpenProjectSettings).toHaveBeenCalledOnce();
  });

  it('keeps the conversation actions at the top without a redundant workspace heading', () => {
    renderSidebar();

    expect(screen.queryByText('项目伙伴')).not.toBeInTheDocument();
    expect(screen.queryByRole('heading', { name: '我的项目空间' })).not.toBeInTheDocument();
    expect(screen.getByTestId('agent-conversation-sidebar-header')).toHaveClass('justify-end', 'bg-surface-tertiary');
    expect(screen.getByTestId('agent-conversation-sidebar-header')).not.toHaveClass('border-b');
  });

  it('uses square corners for each project contact card', () => {
    renderSidebar();

    const contactButton = screen.getByTestId('project-agent-chat-agent-a');
    const contactCard = contactButton.parentElement?.parentElement;
    expect(contactCard).toHaveClass('rounded-none');
    expect(contactCard).not.toHaveClass('rounded-xl');
  });

  it('shows contacts, keeps pinned contacts first, and opens that contact’s session list', () => {
    const callbacks = renderSidebar();
    const contacts = screen.getAllByTestId(/^project-agent-chat-/);
    expect(contacts[0]).toHaveAttribute('data-testid', 'project-agent-chat-agent-b');
    expect(screen.getByText('小明')).toBeInTheDocument();
    expect(screen.getByText('有新消息')).toBeInTheDocument();
    expect(within(screen.getByTestId('agent-status-row-agent-a')).getByText('2')).toBeInTheDocument();
    expect(screen.queryByText('等待开始')).not.toBeInTheDocument();

    fireEvent.click(screen.getByTestId('project-agent-chat-agent-a'));
    expect(callbacks.onSelectAgent).toHaveBeenCalledWith('agent-a');
  });

  it('expands sessions directly below the selected contact and removes the back/settings actions', () => {
    const callbacks = renderSidebar({ selectedAgentId: 'agent-a' });

    expect(screen.getByTestId('project-agent-chat-agent-a')).toHaveAttribute('aria-expanded', 'true');
    expect(screen.getByTestId('project-agent-chat-agent-a').parentElement?.parentElement).toHaveAttribute('data-selected', 'true');
    expect(screen.getByTestId('project-agent-chat-agent-b').parentElement?.parentElement).toHaveAttribute('data-selected', 'false');
    expect(screen.getByLabelText('小明的会话列表')).toBeInTheDocument();
    expect(screen.getByLabelText('小明的会话列表').parentElement?.parentElement).toHaveAttribute('data-state', 'open');
    expect(screen.getByLabelText('小红的会话列表').parentElement?.parentElement).toHaveAttribute('data-state', 'closed');
    expect(screen.getByText('第一次聊天')).toBeInTheDocument();
    expect(screen.queryByText('小明的对话')).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '返回伙伴列表' })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '伙伴设置' })).not.toBeInTheDocument();

    fireEvent.click(screen.getByTestId('project-agent-chat-agent-a'));
    expect(callbacks.onSelectAgent).toHaveBeenCalledWith('agent-a');
  });

  it('marks the selected session so the active conversation remains visible', () => {
    renderSidebar({ selectedAgentId: 'agent-a', selectedSessionId: 'ses-a' });

    expect(screen.getByText('第一次聊天').closest('button')).toHaveAttribute('aria-current', 'page');
  });

  it('keeps the contact disabled while opening without showing a transient work status', () => {
    renderSidebar({ startingAgentId: 'agent-a' });

    const contact = screen.getByTestId('project-agent-chat-agent-a');
    expect(screen.queryByText('处理中')).not.toBeInTheDocument();
    expect(contact).toBeDisabled();
    expect(contact).toHaveAttribute('aria-busy', 'true');
  });

  it('hides the recent conversation status after the session has been read', () => {
    renderSidebar({
      conversationSessions: [metadata('ses-a', 'agent-a', { unreadCount: 0 })],
    });

    expect(screen.queryByText('最近有对话')).not.toBeInTheDocument();
    expect(screen.queryByText('等待开始')).not.toBeInTheDocument();
  });

  it('centers the pin and archive actions vertically on the contact card', () => {
    renderSidebar();

    const pinButton = screen.getByRole('button', { name: '置顶伙伴' });
    const actionRail = pinButton.parentElement;
    expect(actionRail).toHaveClass('top-1/2', '-translate-y-1/2', 'flex-col');
  });

  it('lists sessions compactly, shows five by default, and expands the rest', () => {
    const longMessage = '这是最新的一条消息，内容很长，超出侧栏宽度后应该隐藏并显示省略号。';
    const allSessions = Array.from({ length: 6 }, (_, index) => session(`ses-${index + 1}`, `会话${index + 1}`));
    renderSidebar({
      sessions: allSessions,
      sessionMessagesBySessionId: {
        'ses-1': [{ role: 'user', content: longMessage }],
      },
      conversationSessions: allSessions.map((item, index) => metadata(item.id as string, 'agent-a', {
        updatedAt: `2026-07-${String(30 - index).padStart(2, '0')}T00:02:00.000Z`,
      })),
      selectedAgentId: 'agent-a',
    });

    expect(screen.getByTitle(longMessage)).toHaveTextContent(/…$/u);
    expect(screen.queryByText('其他会话')).not.toBeInTheDocument();
    const moreSessions = screen.getByRole('button', { name: '更多会话（1）' });
    expect(moreSessions).toHaveAttribute('aria-expanded', 'false');

    fireEvent.click(moreSessions);
    expect(moreSessions).toHaveAttribute('aria-expanded', 'true');

    fireEvent.click(moreSessions);
    expect(moreSessions).toHaveAttribute('aria-expanded', 'false');
  });

  it('removes the session bubble icon and keeps archive hidden until the card is hovered', () => {
    renderSidebar({ selectedAgentId: 'agent-a' });

    const preview = screen.getByText('第一次聊天');
    const sessionButton = preview.closest('button');
    const archiveButton = screen.getByRole('button', { name: '归档会话 第一次聊天' });
    expect(sessionButton?.querySelector('svg')).toBeNull();
    expect(archiveButton).toHaveClass('opacity-0', 'group-hover/session-card:opacity-100');
  });

  it('creates a contact from the required basics and confirms archive actions', async () => {
    const callbacks = renderSidebar();
    fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
    fireEvent.change(screen.getByLabelText(/伙伴名称/), { target: { value: '小李' } });
    fireEvent.change(screen.getByLabelText(/职责简述/), { target: { value: '负责整理资料。' } });
    fireEvent.click(screen.getByRole('button', { name: '更换头像' }));
    fireEvent.click(screen.getByRole('button', { name: '选择头像：像素伙伴 3' }));
    fireEvent.click(screen.getByRole('button', { name: '创建并开始对话' }));
    await waitFor(() => expect(callbacks.onCreateAgent).toHaveBeenCalledWith({
      name: '小李',
      avatarId: 'avatar-03',
      model: modelOption.modelRef,
      responsibility: '负责整理资料。',
      prompt: '',
      skillIds: [],
    }));

    fireEvent.click(screen.getByLabelText('归档伙伴 小明'));
    expect(screen.getByRole('dialog', { name: '归档伙伴“小明”？' })).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: '取消' }));
    expect(callbacks.onArchiveAgent).not.toHaveBeenCalled();
  });

  it('shows advanced creation settings directly below the basics', () => {
    renderSidebar();
    fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));

    expect(screen.getByText('高级设置（提示词与技能）')).toBeInTheDocument();
    expect(screen.getByLabelText('Agent 提示词')).toBeInTheDocument();
    expect(screen.getByRole('heading', { name: '绑定技能' })).toBeInTheDocument();
  });

  it('places archived contacts and sessions below active items and exposes restore actions', () => {
    const archivedAgent = agent('agent-archived', '旧伙伴', { archivedAt: '2026-07-29T00:00:00.000Z' });
    renderSidebar({
      agents: [agent('agent-a', '小明'), archivedAgent],
      conversationSessions: [
        metadata('ses-a', 'agent-a'),
        metadata('ses-old', 'agent-a', { archivedAt: '2026-07-29T00:00:00.000Z' }),
      ],
      selectedAgentId: 'agent-a',
    });
    fireEvent.click(screen.getByRole('button', { name: /已归档伙伴/ }));
    fireEvent.click(screen.getByRole('button', { name: '已归档会话（1）' }));
    expect(screen.getByText('旧伙伴')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '恢复伙伴 旧伙伴' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '永久删除伙伴 旧伙伴' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '恢复会话 新对话' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '永久删除会话 新对话' })).toBeInTheDocument();
  });

  it('requires explicit confirmation before permanently deleting an archived session', async () => {
    const callbacks = renderSidebar({
      selectedAgentId: 'agent-a',
      conversationSessions: [metadata('ses-old', 'agent-a', { archivedAt: '2026-07-29T00:00:00.000Z' })],
    });

    fireEvent.click(screen.getByText('已归档会话（1）'));
    fireEvent.click(screen.getByRole('button', { name: '永久删除会话 新对话' }));
    expect(screen.getByRole('dialog', { name: '永久删除会话“新对话”？' })).toBeInTheDocument();
    expect(callbacks.onDeleteSession).not.toHaveBeenCalled();
    fireEvent.click(screen.getByRole('button', { name: '永久删除' }));
    await waitFor(() => expect(callbacks.onDeleteSession).toHaveBeenCalledWith('ses-old'));
  });

  it('requires explicit confirmation before permanently deleting archived records', async () => {
    const archivedAgent = agent('agent-archived', '旧伙伴', { archivedAt: '2026-07-29T00:00:00.000Z' });
    const callbacks = renderSidebar({
      agents: [archivedAgent],
      conversationSessions: [metadata('ses-old', 'agent-archived', { archivedAt: '2026-07-29T00:00:00.000Z' })],
    });

    fireEvent.click(screen.getByRole('button', { name: /已归档伙伴/ }));
    fireEvent.click(screen.getByRole('button', { name: '永久删除伙伴 旧伙伴' }));
    expect(screen.getByRole('dialog', { name: '永久删除伙伴“旧伙伴”？' })).toBeInTheDocument();
    expect(callbacks.onDeleteAgent).not.toHaveBeenCalled();
    fireEvent.click(screen.getByRole('button', { name: '永久删除' }));
    await waitFor(() => expect(callbacks.onDeleteAgent).toHaveBeenCalledWith(archivedAgent));
  });

  it('keeps permanent deletion unavailable for archived built-in contacts', () => {
    renderSidebar({
      agents: [agent('built-in-archived', '内置伙伴', { builtIn: true, archivedAt: '2026-07-29T00:00:00.000Z' })],
    });

    fireEvent.click(screen.getByRole('button', { name: /已归档伙伴/ }));
    expect(screen.queryByRole('button', { name: '永久删除伙伴 内置伙伴' })).not.toBeInTheDocument();
  });
});
