import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ProjectConfiguration } from '@/pages/ProjectConfiguration';
import { useOpencodeStore } from '@/stores/opencode';
import { useProjectConversationStore } from '@/stores/project-conversations';
import { useProjectConfigStore } from '@/stores/project-config';
import { useProviderStore } from '@/stores/providers';
import { createProjectConfig } from '../../shared/project-config';
import { createProjectConversationState, removeProjectSessionMetadata, upsertProjectSessionMetadata } from '../../shared/project-conversations';

const hostApiFetchMock = vi.fn();
vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args) }));

describe('ProjectConfiguration', () => {
  const project = { id: 'prj_config', path: 'D:/repo/config', name: 'config', createdAt: '', updatedAt: '', lastOpenedAt: '' };
  const account = {
    id: 'openai-account',
    vendorId: 'openai',
    label: 'OpenAI',
    authMode: 'api_key' as const,
    model: 'gpt-4o-mini',
    enabled: true,
    isDefault: true,
    createdAt: '2026-07-11T00:00:00.000Z',
    updatedAt: '2026-07-11T00:00:00.000Z',
  };
  const status = {
    id: account.id,
    name: account.label,
    type: account.vendorId,
    model: account.model,
    enabled: true,
    hasKey: true,
    keyMasked: 'sk-***',
    createdAt: account.createdAt,
    updatedAt: account.updatedAt,
  };

  beforeEach(() => {
    vi.clearAllMocks();
    Object.defineProperty(window, 'matchMedia', {
      configurable: true,
      value: vi.fn().mockImplementation((query: string) => ({
        matches: query === '(prefers-reduced-motion: reduce)',
        media: query,
        onchange: null,
        addListener: vi.fn(),
        removeListener: vi.fn(),
        addEventListener: vi.fn(),
        removeEventListener: vi.fn(),
        dispatchEvent: vi.fn(),
      })),
    });
    const config = createProjectConfig();
    useOpencodeStore.setState({ activeProject: project, projects: [project] });
    useProjectConfigStore.setState({ configsByProjectId: { [project.id]: config }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
    useProviderStore.setState({
      accounts: [account],
      statuses: [status],
      defaultAccountId: account.id,
      loading: false,
      error: null,
    });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
      if (path === '/api/opencode/skills') return { skills: [] };
      if (path === '/api/provider-accounts') return [account];
      if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
      if (path === '/api/provider-vendors') return [];
      if (path === '/api/provider-accounts/default') return { accountId: account.id };
      if (path === '/api/opencode/projects/config' && init?.method === 'PUT') return { success: true, config: { ...(JSON.parse(init.body as string).config), initialized: true }, 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}`);
    });
  });

  it('shows an empty contact list for a new project', async () => {
    render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);

    expect(await screen.findByText('配置你的项目空间')).toBeInTheDocument();
    expect(screen.getByText(/新项目还没有联系人/)).toBeInTheDocument();
    expect(screen.getByText('还没有联系人，点击右上角创建第一个。')).toBeInTheDocument();
    expect(screen.queryAllByTestId(/^project-agent-/)).toHaveLength(0);
  });

  it('creates a contact with the required basics and leaves advanced settings empty', async () => {
    render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: '新增伙伴' }));

    expect(screen.getByRole('dialog', { name: '创建项目联系人' })).toBeInTheDocument();
    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.change(screen.getByLabelText('Agent 提示词'), { target: { value: '先拆解目标，再给出执行步骤。' } });
    fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
    await waitFor(() => expect(screen.queryByRole('dialog', { name: '创建项目联系人' })).not.toBeInTheDocument());
    fireEvent.click(screen.getByRole('button', { name: '确认并完成初始化' }));

    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/config', expect.objectContaining({ method: 'PUT' })));
    const saveCall = hostApiFetchMock.mock.calls.find(([path, init]) => path === '/api/opencode/projects/config' && (init as RequestInit | undefined)?.method === 'PUT');
    const savedConfig = JSON.parse((saveCall?.[1] as RequestInit).body as string).config;
    expect(savedConfig.initialized).toBe(true);
    expect(savedConfig.agents).toEqual([expect.objectContaining({
      name: '小明',
      avatarId: 'avatar-03',
      model: 'openai/gpt-4o-mini',
      prompt: '先拆解目标，再给出执行步骤。',
      skillIds: [],
      responsibility: expect.objectContaining({ mission: '负责把想法拆成步骤。' }),
    })]);
  });

  it('requires confirmation before archiving a contact in settings', async () => {
    const config = createProjectConfig();
    const contact = {
      id: 'agent-settings',
      avatarId: 'avatar-01',
      roleName: '项目伙伴',
      name: '小红',
      builtIn: false,
      enabled: true,
      model: 'openai/gpt-4o-mini',
      skillIds: [],
      responsibility: { mission: '负责设计体验。', owns: [], boundaries: [], collaborators: [], principles: [] },
      prompt: '',
      archivedAt: null,
      pinned: false,
    };
    const configWithContact = { ...config, initialized: true, agents: [contact] };
    useProjectConfigStore.setState({ configsByProjectId: { [project.id]: configWithContact }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config: configWithContact, knowledgeFiles: [] };
      if (path === '/api/opencode/skills') return { skills: [] };
      if (path === '/api/provider-accounts') return [account];
      if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
      if (path === '/api/provider-vendors') return [];
      if (path === '/api/provider-accounts/default') return { accountId: account.id };
      if (path === '/api/opencode/projects/conversations') return { state: { schemaVersion: 1, sessions: [], updatedAt: new Date().toISOString() } };
      if (path === '/api/opencode/projects/config' && init?.method === 'PUT') return { success: true, config: JSON.parse(init.body as string).config, knowledgeFiles: [] };
      throw new Error(`Unexpected path ${path}`);
    });

    render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
    fireEvent.click(await screen.findByTestId('project-agent-agent-settings'));
    fireEvent.click(screen.getByRole('button', { name: '归档联系人' }));
    expect(screen.getByRole('dialog', { name: '归档联系人“小红”？' })).toBeInTheDocument();
    expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/opencode/projects/config')).toBe(false);
    fireEvent.click(screen.getByRole('button', { name: '取消' }));
    expect(screen.queryByRole('dialog', { name: '归档联系人“小红”？' })).not.toBeInTheDocument();
  });

  it('requires explicit confirmation before removing a project', async () => {
    render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
    fireEvent.click(await screen.findByRole('button', { name: '删除项目' }));

    const dialog = screen.getByRole('dialog', { name: `删除项目“${project.name}”？` });
    expect(dialog).toHaveTextContent('不会删除磁盘中的项目文件');
    expect(hostApiFetchMock).not.toHaveBeenCalledWith('/api/opencode/projects/remove', expect.anything());

    fireEvent.click(screen.getByRole('button', { name: '确认删除项目' }));
    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/remove', {
      method: 'POST',
      body: JSON.stringify({ projectId: project.id }),
    }));
    expect(useOpencodeStore.getState().projects).toEqual([]);
    expect(useOpencodeStore.getState().activeProject).toBeNull();
  });

  it('permanently deletes archived custom contacts and their sessions', async () => {
    const contact = {
      id: 'agent-archived-delete',
      avatarId: 'avatar-01',
      roleName: '项目伙伴',
      name: '待删除伙伴',
      builtIn: false,
      enabled: true,
      model: 'openai/gpt-4o-mini',
      skillIds: [],
      responsibility: { mission: '负责清理测试数据。', owns: [], boundaries: [], collaborators: [], principles: [] },
      prompt: '',
      archivedAt: '2026-07-29T00:00:00.000Z',
      pinned: false,
    };
    const config = { ...createProjectConfig(), initialized: true, agents: [contact] };
    let conversationState = upsertProjectSessionMetadata(
      createProjectConversationState(),
      'ses-archived-delete',
      contact.id,
      '2026-07-29T00:00:00.000Z',
    );
    conversationState = {
      ...conversationState,
      sessions: conversationState.sessions.map((session) => ({ ...session, archivedAt: '2026-07-29T00:00:00.000Z' })),
    };
    useOpencodeStore.setState({ activeProject: project, projects: [project], sessions: [{ id: 'ses-archived-delete', title: '旧会话' }] });
    useProjectConversationStore.setState({ statesByProjectId: { [project.id]: conversationState } });
    useProjectConfigStore.setState({ configsByProjectId: { [project.id]: config }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
    hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
      if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
      if (path === '/api/opencode/skills') return { skills: [] };
      if (path === '/api/provider-accounts') return [account];
      if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
      if (path === '/api/provider-vendors') return [];
      if (path === '/api/provider-accounts/default') return { accountId: account.id };
      if (path === `/api/opencode/projects/conversations?projectId=${project.id}`) return { state: conversationState };
      if (path === '/api/opencode/sessions/ses-archived-delete' && init?.method === 'DELETE') return { success: true };
      if (path === '/api/opencode/projects/conversations' && init?.method === 'POST') {
        conversationState = removeProjectSessionMetadata(conversationState, 'ses-archived-delete');
        return { success: true, state: conversationState };
      }
      if (path === '/api/opencode/projects/config' && init?.method === 'PUT') return { success: true, config: JSON.parse(init.body as string).config, knowledgeFiles: [] };
      throw new Error(`Unexpected path ${path}`);
    });

    render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
    const deleteButton = await screen.findByRole('button', { name: '永久删除联系人 待删除伙伴' });
    fireEvent.click(deleteButton);
    expect(await screen.findByRole('dialog', { name: '永久删除联系人“待删除伙伴”？' })).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: '永久删除' }));

    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/sessions/ses-archived-delete', { method: 'DELETE' }));
    await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/conversations', expect.objectContaining({ method: 'POST' })));
    await waitFor(() => {
      const saveCall = hostApiFetchMock.mock.calls.find(([path, init]) => path === '/api/opencode/projects/config' && (init as RequestInit | undefined)?.method === 'PUT');
      expect(saveCall).toBeTruthy();
      expect(JSON.parse((saveCall?.[1] as RequestInit).body as string).config.agents).toEqual([]);
    });
  });
});
