import {
  AGENT_DOCUMENT_CATEGORY,
  AGENT_DOCUMENT_FILE_TYPE,
  AGENT_DOCUMENT_SKILL_CATEGORY,
  AGENT_SIGNAL_SOURCE_TYPE,
} from '@lobechat/const';
import { fireEvent, render, screen } from '@testing-library/react';
import type { ReactNode } from 'react';
import type * as ReactRouterDom from 'react-router';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import AgentDocumentsGroup from './AgentDocumentsGroup';

const useClientDataSWR = vi.fn();
const modalConfirm = vi.hoisted(() => vi.fn());
const messageError = vi.hoisted(() => vi.fn());
const messageSuccess = vi.hoisted(() => vi.fn());
const navigateMock = vi.hoisted(() => vi.fn());
const openDocumentMock = vi.hoisted(() => vi.fn());
const removeDocumentMock = vi.hoisted(() => vi.fn());
const useParamsMock = vi.hoisted(() => vi.fn());

vi.mock('@lobehub/ui/base-ui', () => ({
  confirmModal: modalConfirm,
}));

vi.mock('@lobehub/ui', () => ({
  Accordion: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
  AccordionItem: ({ children, title }: { children?: ReactNode; title?: ReactNode }) => (
    <div>
      {title}
      {children}
    </div>
  ),
  ActionIcon: ({ onClick, title }: { onClick?: (e: React.MouseEvent) => void; title?: string }) => (
    <button aria-label={title} onClick={onClick}>
      {title}
    </button>
  ),
  Center: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
  Empty: ({ description }: { description?: ReactNode }) => <div>{description}</div>,
  Flexbox: ({
    children,
    onClick,
    ...props
  }: {
    children?: ReactNode;
    onClick?: () => void;
    [key: string]: unknown;
  }) => (
    <div onClick={onClick} {...props}>
      {children}
    </div>
  ),
  Text: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
}));

vi.mock('antd', () => ({
  App: {
    useApp: () => ({
      message: { error: messageError, success: messageSuccess },
    }),
  },
}));

vi.mock('@/components/NeuralNetworkLoading', () => ({
  default: () => <div data-testid="neural-network-loading" />,
}));

vi.mock('@/libs/swr', () => ({
  useClientDataSWR: (...args: unknown[]) => useClientDataSWR(...args),
}));

vi.mock('react-i18next', () => ({
  useTranslation: () => ({
    t: (key: string, options?: { time?: string }) =>
      (
        ({
          'workingPanel.resources.empty': 'No agent documents yet',
          'workingPanel.resources.error': 'Failed to load resources',
          'workingPanel.resources.filter.documents': 'Documents',
          'workingPanel.resources.filter.skills': 'Skills',
          'workingPanel.resources.filter.web': 'Web',
          'workingPanel.resources.updatedAt': `Updated ${options?.time}`,
          'workingPanel.skills.empty': 'No skills found',
          'workingPanel.skills.section.agent': 'Agent skills',
          'workingPanel.skills.section.project': 'Project skills',
          'workingPanel.skills.section.user': 'User skills',
        }) as Record<string, string>
      )[key] || key,
  }),
}));

vi.mock('@/features/AgentDocumentsExplorer', () => ({
  DocumentExplorerTree: ({
    data,
    onOpenDocument,
  }: {
    data: { documentId: string; id: string; title?: string }[];
    onOpenDocument?: (documentId: string, agentDocumentId?: string) => void;
  }) => (
    <div data-doc-count={data.length} data-testid="document-explorer-tree">
      {data.map((doc) => (
        <button
          data-testid={`tree-open-${doc.id}`}
          key={doc.id}
          onClick={() => onOpenDocument?.(doc.documentId, doc.id)}
        >
          {doc.title}
        </button>
      ))}
    </div>
  ),
}));

vi.mock('@/features/Workspace/useWorkspaceAwareNavigate', () => ({
  useWorkspaceAwareNavigate: () => navigateMock,
}));

vi.mock('@/features/SkillsList', () => {
  type Item = { fileCount?: number; id: string; name: string };
  const SkillsList = ({
    items,
    onOpenFile,
    onOpenSkill,
  }: {
    items: Item[];
    onOpenFile?: (item: Item, relativePath: string) => void;
    onOpenSkill?: (item: Item) => void;
  }) => (
    <div data-testid="skills-list">
      {items.map((item) => (
        <div data-skill-id={item.id} key={item.id}>
          <button onClick={() => onOpenSkill?.(item)}>{item.name}</button>
          <span data-testid={`skill-${item.id}-count`}>{item.fileCount}</span>
          <button
            data-testid={`skill-${item.id}-open-skill-md`}
            onClick={() => onOpenFile?.(item, 'SKILL.md')}
          >
            open-skill-md
          </button>
        </div>
      ))}
    </div>
  );
  // SkillSection is a thin presentational wrapper — the real one swaps the
  // children for an empty placeholder when `isEmpty` is true. We mirror that
  // here so co-located assertions (e.g. "skills-list does not render when no
  // bundles are present") line up with production behavior.
  const SkillSection = ({
    children,
    emptyText,
    isEmpty,
    sectionHeader,
  }: {
    children?: ReactNode;
    emptyText?: string;
    isEmpty?: boolean;
    sectionHeader?: { count?: number; title: string };
  }) => (
    <div data-testid={sectionHeader ? `skill-section-${sectionHeader.title}` : 'skill-section'}>
      {sectionHeader && (
        <div data-testid="skill-section-header">
          <span>{sectionHeader.title}</span>
          {typeof sectionHeader.count === 'number' && <span>{sectionHeader.count}</span>}
        </div>
      )}
      {isEmpty ? <div data-testid="skill-section-empty">{emptyText}</div> : children}
    </div>
  );
  const useProjectSkills = () => ({
    isLoading: false,
    items: [],
    onOpenFile: () => undefined,
    onOpenSkill: () => undefined,
    raw: undefined,
  });
  return { SkillSection, SkillsList, useProjectSkills };
});

// UserLevelSkills owns its own store wiring and is exercised separately. We
// stub it (component + the lifted hook) so AgentDocumentsGroup's render logic
// can be tested without dragging the tool store into the working-sidebar
// tests. Default to an empty user-skill list so the empty-state branch is
// reachable; individual tests can re-mock before render to override.
vi.mock('./UserLevelSkills', () => ({
  default: () => null,
  useUserSkills: () => [],
}));
vi.mock('@/features/ChatInput/InputEditor/ActionTag/skillDragData', () => ({
  startSkillDrag: () => undefined,
}));

vi.mock('@/services/agentDocument', () => ({
  agentDocumentSWRKeys: {
    documents: (agentId: string) => ['agent:documents', agentId],
    documentsList: (agentId: string) => ['agent:documentsList', agentId],
  },
  agentDocumentService: {
    getDocuments: vi.fn(),
    removeDocument: removeDocumentMock,
  },
}));

vi.mock('@/store/agent', () => ({
  useAgentStore: (selector: (state: { activeAgentId: string; agentMap: object }) => unknown) =>
    selector({ activeAgentId: 'agent-1', agentMap: {} }),
}));

vi.mock('@/store/chat', () => ({
  useChatStore: (selector: (state: { openDocument: typeof openDocumentMock }) => unknown) =>
    selector({ openDocument: openDocumentMock }),
}));

vi.mock('react-router', async () => {
  const actual = await vi.importActual<typeof ReactRouterDom>('react-router');
  return {
    ...actual,
    useParams: useParamsMock,
  };
});

const skillBundleRow = {
  category: AGENT_DOCUMENT_SKILL_CATEGORY,
  createdAt: new Date('2026-05-09T00:00:00Z'),
  description: 'Use for YouTube comments',
  documentId: 'skill-bundle-doc',
  fileType: 'skills/bundle',
  filename: 'youtube-comment-retrieval-workflow',
  id: 'skill-bundle-row',
  isFolder: true,
  isSkillBundle: true,
  isSkillIndex: false,
  parentId: null,
  sourceType: AGENT_SIGNAL_SOURCE_TYPE,
  templateId: 'agent-skill',
  title: 'YouTube Comment Retrieval Workflow',
  updatedAt: new Date(),
};

const skillIndexRow = {
  category: AGENT_DOCUMENT_SKILL_CATEGORY,
  createdAt: new Date('2026-05-09T00:00:00Z'),
  description: 'Use for YouTube comments',
  documentId: 'skill-index-doc',
  fileType: 'skills/index',
  filename: 'SKILL.md',
  id: 'skill-index-row',
  isFolder: false,
  isSkillBundle: false,
  isSkillIndex: true,
  parentId: 'skill-bundle-doc',
  sourceType: AGENT_SIGNAL_SOURCE_TYPE,
  templateId: 'agent-skill',
  title: 'SKILL.md',
  updatedAt: new Date(),
};

const fileDocRow = {
  category: AGENT_DOCUMENT_CATEGORY,
  createdAt: new Date('2026-04-16T00:00:00Z'),
  description: 'A short brief',
  documentId: 'doc-content-1',
  fileType: AGENT_DOCUMENT_FILE_TYPE,
  filename: 'brief.md',
  id: 'doc-1',
  isFolder: false,
  isSkillBundle: false,
  isSkillIndex: false,
  parentId: null,
  sourceType: 'file',
  templateId: null,
  title: 'Brief',
  updatedAt: new Date(),
};

const webDocRow = {
  category: 'web',
  createdAt: new Date('2026-04-16T00:00:00Z'),
  description: 'Crawled page',
  documentId: 'doc-content-2',
  fileType: 'article',
  filename: 'example.com',
  id: 'doc-2',
  isFolder: false,
  isSkillBundle: false,
  isSkillIndex: false,
  parentId: null,
  sourceType: 'web',
  templateId: null,
  title: 'Example',
  updatedAt: new Date(),
};

describe('AgentDocumentsGroup', () => {
  beforeEach(() => {
    useClientDataSWR.mockReset();
    modalConfirm.mockReset();
    messageError.mockReset();
    messageSuccess.mockReset();
    navigateMock.mockReset();
    openDocumentMock.mockReset();
    removeDocumentMock.mockReset();
    removeDocumentMock.mockResolvedValue({ deleted: true, id: 'doc-1' });
    useParamsMock.mockReturnValue({});
  });

  it('defaults to the Skills tab and renders skill bundles via SkillsList', () => {
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow, fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    const list = screen.getByTestId('skills-list');
    expect(list).toBeInTheDocument();
    expect(screen.getByText('YouTube Comment Retrieval Workflow')).toBeInTheDocument();
    // bundle's file children count (the SKILL.md row collapsed inside the expand panel)
    expect(screen.getByTestId('skill-skill-bundle-doc-count')).toHaveTextContent('1');
    // file / web docs should not leak into the skills tab
    expect(screen.queryByText('Brief')).not.toBeInTheDocument();
    expect(screen.queryByText('Example')).not.toBeInTheDocument();
  });

  it('opens the SKILL.md document in the portal when clicking a skill bundle row', () => {
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    fireEvent.click(screen.getByText('YouTube Comment Retrieval Workflow'));
    expect(openDocumentMock).toHaveBeenCalledWith('skill-index-doc', 'skill-index-row');
    expect(navigateMock).not.toHaveBeenCalled();
  });

  it('falls back to the bundle id when opening an orphan skill bundle', () => {
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow], // no SKILL.md child
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    fireEvent.click(screen.getByText('YouTube Comment Retrieval Workflow'));
    expect(openDocumentMock).toHaveBeenCalledWith('skill-bundle-doc', 'skill-bundle-row');
    expect(navigateMock).not.toHaveBeenCalled();
  });

  it('opens a child file by relative path through onOpenFile', () => {
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    fireEvent.click(screen.getByTestId('skill-skill-bundle-doc-open-skill-md'));
    expect(openDocumentMock).toHaveBeenCalledWith('skill-index-doc', 'skill-index-row');
    expect(navigateMock).not.toHaveBeenCalled();
  });

  it('routes directly when opening a skill from document mode', () => {
    useParamsMock.mockReturnValue({ docId: 'doc-content-1' });
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup activeFilter="skills" />);

    fireEvent.click(screen.getByText('YouTube Comment Retrieval Workflow'));
    expect(navigateMock).toHaveBeenCalledWith('/agent/agent-1/docs/skill-index-doc');
    expect(openDocumentMock).not.toHaveBeenCalled();
  });

  it('renders the document tree when switching to the Documents tab', () => {
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow, fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    fireEvent.click(screen.getByText('Documents'));

    const tree = screen.getByTestId('document-explorer-tree');
    expect(tree).toBeInTheDocument();
    // Skill bundle, skill index, and web items are filtered out before reaching
    // the tree — only the file-backed document survives.
    expect(tree).toHaveAttribute('data-doc-count', '1');
  });

  it('opens document tree files in the portal by default', () => {
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow, fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    fireEvent.click(screen.getByText('Documents'));
    fireEvent.click(screen.getByTestId('tree-open-doc-1'));

    expect(openDocumentMock).toHaveBeenCalledWith('doc-content-1', 'doc-1');
    expect(navigateMock).not.toHaveBeenCalled();
  });

  it('routes document tree files directly in document mode', () => {
    useParamsMock.mockReturnValue({ docId: 'doc-content-1' });
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow, fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    fireEvent.click(screen.getByTestId('tree-open-doc-1'));

    expect(navigateMock).toHaveBeenCalledWith('/agent/agent-1/docs/doc-content-1');
    expect(openDocumentMock).not.toHaveBeenCalled();
  });

  it('defaults to Documents first in document mode and keeps Skills as the second tab', () => {
    useParamsMock.mockReturnValue({ docId: 'doc-content-1' });
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow, fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    expect(screen.getAllByRole('tab').map((tab) => tab.textContent)).toEqual([
      'Documents',
      'Skills',
    ]);
    expect(screen.queryByText('Web')).not.toBeInTheDocument();

    const tree = screen.getByTestId('document-explorer-tree');
    expect(tree).toBeInTheDocument();
    expect(tree).toHaveAttribute('data-doc-count', '1');
    expect(screen.queryByTestId('skills-list')).not.toBeInTheDocument();

    fireEvent.click(screen.getByText('Skills'));
    expect(screen.getByTestId('skills-list')).toBeInTheDocument();
  });

  it('can be controlled by an outer page-mode tab bar', () => {
    useParamsMock.mockReturnValue({ docId: 'doc-content-1' });
    useClientDataSWR.mockReturnValue({
      data: [skillBundleRow, skillIndexRow, fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    const { rerender } = render(
      <AgentDocumentsGroup activeFilter="skills" showFilterTabs={false} />,
    );

    expect(screen.queryByRole('tab')).not.toBeInTheDocument();
    expect(screen.getByTestId('skills-list')).toBeInTheDocument();
    expect(screen.queryByTestId('document-explorer-tree')).not.toBeInTheDocument();

    rerender(<AgentDocumentsGroup activeFilter="documents" showFilterTabs={false} />);

    expect(screen.queryByRole('tab')).not.toBeInTheDocument();
    expect(screen.getByTestId('document-explorer-tree')).toBeInTheDocument();
    expect(screen.queryByTestId('skills-list')).not.toBeInTheDocument();
  });

  it('resets to Documents when entering document mode', () => {
    useClientDataSWR.mockReturnValue({
      data: [fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    const { rerender } = render(<AgentDocumentsGroup />);
    fireEvent.click(screen.getByText('Web'));
    expect(screen.getByText('Example')).toBeInTheDocument();

    useParamsMock.mockReturnValue({ docId: 'doc-content-1' });
    rerender(<AgentDocumentsGroup style={{ minHeight: 0 }} />);

    expect(screen.getAllByRole('tab').map((tab) => tab.textContent)).toEqual([
      'Documents',
      'Skills',
    ]);
    expect(screen.getByTestId('document-explorer-tree')).toBeInTheDocument();
    expect(screen.queryByText('Example')).not.toBeInTheDocument();
  });

  it('renders web items as cards in the Web tab and supports deletion', async () => {
    const mutate = vi.fn().mockResolvedValue(undefined);
    useClientDataSWR.mockReturnValue({
      data: [fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate,
    });

    render(<AgentDocumentsGroup />);
    fireEvent.click(screen.getByText('Web'));

    expect(screen.getByText('Example')).toBeInTheDocument();
    expect(screen.queryByText('Brief')).not.toBeInTheDocument();

    fireEvent.click(screen.getByText('Example'));
    expect(openDocumentMock).toHaveBeenCalledWith('doc-content-2', 'doc-2');
    expect(navigateMock).not.toHaveBeenCalled();
    openDocumentMock.mockClear();

    fireEvent.click(screen.getByLabelText('delete'));
    const [firstConfirmCall] = modalConfirm.mock.calls;
    const [{ onOk }] = firstConfirmCall;
    await onOk();

    expect(removeDocumentMock).toHaveBeenCalledWith({
      agentId: 'agent-1',
      documentId: 'doc-content-2',
      id: 'doc-2',
    });
    expect(mutate).toHaveBeenCalled();
    expect(navigateMock).not.toHaveBeenCalled();
    expect(openDocumentMock).not.toHaveBeenCalled();
    expect(messageSuccess).toHaveBeenCalledWith('workingPanel.resources.deleteSuccess');
  });

  it('falls back to a single empty placeholder when every skill source is empty', () => {
    useClientDataSWR.mockReturnValue({
      data: [fileDocRow, webDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    // No agent bundles, no working dir (no Project section), no user-installed
    // skills → renderSkills collapses to the global "No skills found"
    // placeholder rather than rendering an empty section per source.
    expect(screen.getByText('No skills found')).toBeInTheDocument();
    expect(screen.queryByTestId('skills-list')).not.toBeInTheDocument();
  });

  it('shows the web empty state when no web items are present', () => {
    useClientDataSWR.mockReturnValue({
      data: [fileDocRow],
      error: undefined,
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);
    fireEvent.click(screen.getByText('Web'));

    expect(screen.getByText('No agent documents yet')).toBeInTheDocument();
  });

  it('renders error state when SWR returns an error', () => {
    useClientDataSWR.mockReturnValue({
      data: [],
      error: new Error('oops'),
      isLoading: false,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);

    expect(screen.getByText('Failed to load resources')).toBeInTheDocument();
  });

  it('renders the loading spinner while data is being fetched', () => {
    useClientDataSWR.mockReturnValue({
      data: undefined,
      error: undefined,
      isLoading: true,
      mutate: vi.fn(),
    });

    render(<AgentDocumentsGroup />);
    expect(screen.getByTestId('neural-network-loading')).toBeInTheDocument();
  });
});
