// @vitest-environment happy-dom
//
// Empty-space right-click and select-all. Renders the real component with
// react-dom/client, like FileTree.statuses.test.jsx.
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { FileTree } from './FileTree';

const ROOT_ID = 'root::';
const SUB_ID = `${ROOT_ID}Sub`;
const DEEP_ID = `${ROOT_ID}Sub/deep.txt`;
const TOP_ID = `${ROOT_ID}top.txt`;

let container;

beforeEach(() => {
  container = document.createElement('div');
  document.body.appendChild(container);
});

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

// Root is expanded; 'Sub' is collapsed, so deep.txt is not a visible row.
const nodes = () => [
  {
    id: ROOT_ID,
    type: 'folder',
    name: 'Workspace',
    isRoot: true,
    expanded: true,
    children: [
      {
        id: SUB_ID,
        type: 'folder',
        name: 'Sub',
        expanded: false,
        children: [{ id: DEEP_ID, type: 'file', name: 'deep.txt', size: '--' }],
      },
      { id: TOP_ID, type: 'file', name: 'top.txt', size: '--' },
    ],
  },
];

function renderTree(handlers) {
  const root = createRoot(container);
  act(() => {
    root.render(
      <FileTree
        nodes={nodes()}
        setNodes={() => {}}
        mode="workspace"
        selectedIds={new Set([ROOT_ID])}
        anchorId={ROOT_ID}
        onSelect={handlers.onSelect ?? (() => {})}
        sort={{ column: 'name', direction: 'asc' }}
        onSort={() => {}}
        scrollTop={0}
        onScroll={() => {}}
        visible
        rootId={ROOT_ID}
        statuses={null}
        onStatusClick={() => {}}
        pulsePaths={null}
        deletedFolders={null}
        onContextMenuRow={handlers.onContextMenuRow ?? (() => {})}
        onContextMenuEmpty={handlers.onContextMenuEmpty ?? (() => {})}
        onOpenFile={() => {}}
        dropTargetId={null}
        versionFractionOf={null}
      />
    );
  });
  return root;
}

const rightClick = (el) =>
  act(() => {
    el.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }));
  });

describe('FileTree empty-space context menu', () => {
  it('fires the empty callback for the scroll container, not the row callback', () => {
    const onContextMenuEmpty = vi.fn();
    const onContextMenuRow = vi.fn();
    const onSelect = vi.fn();
    const root = renderTree({ onContextMenuEmpty, onContextMenuRow, onSelect });
    try {
      rightClick(container.querySelector('.file-tree-container'));

      expect(onContextMenuEmpty).toHaveBeenCalledTimes(1);
      expect(onContextMenuRow).not.toHaveBeenCalled();
      // Empty space means "no selection", like a real file browser.
      expect(onSelect).toHaveBeenCalledWith(new Set(), null);
    } finally {
      act(() => root.unmount());
    }
  });

  it('fires the empty callback for the list padding above and below the rows', () => {
    const onContextMenuEmpty = vi.fn();
    const root = renderTree({ onContextMenuEmpty });
    try {
      rightClick(container.querySelector('ul.file-tree'));
      expect(onContextMenuEmpty).toHaveBeenCalledTimes(1);
    } finally {
      act(() => root.unmount());
    }
  });

  it('fires only the row callback for a right-click on a row', () => {
    const onContextMenuEmpty = vi.fn();
    const onContextMenuRow = vi.fn();
    const root = renderTree({ onContextMenuEmpty, onContextMenuRow });
    try {
      rightClick(container.querySelector(`[data-node-id="${TOP_ID}"]`));

      expect(onContextMenuRow).toHaveBeenCalledTimes(1);
      expect(onContextMenuEmpty).not.toHaveBeenCalled();
    } finally {
      act(() => root.unmount());
    }
  });

  it('fires neither for a right-click on the sort header', () => {
    const onContextMenuEmpty = vi.fn();
    const onContextMenuRow = vi.fn();
    const root = renderTree({ onContextMenuEmpty, onContextMenuRow });
    try {
      rightClick(container.querySelector('.file-tree-header-col'));

      expect(onContextMenuEmpty).not.toHaveBeenCalled();
      expect(onContextMenuRow).not.toHaveBeenCalled();
    } finally {
      act(() => root.unmount());
    }
  });

  it('hands the empty menu a selectAll that covers visible rows only', () => {
    const onContextMenuEmpty = vi.fn();
    const onSelect = vi.fn();
    const root = renderTree({ onContextMenuEmpty, onSelect });
    try {
      rightClick(container.querySelector('.file-tree-container'));
      const { selectAll } = onContextMenuEmpty.mock.calls[0][1];

      onSelect.mockClear();
      act(() => selectAll());

      const [ids] = onSelect.mock.calls[0];
      expect(ids.has(ROOT_ID)).toBe(true);
      expect(ids.has(SUB_ID)).toBe(true);
      expect(ids.has(TOP_ID)).toBe(true);
      // 'Sub' is collapsed, so its child is not a row and is not selected.
      expect(ids.has(DEEP_ID)).toBe(false);
      expect(ids.size).toBe(3);
    } finally {
      act(() => root.unmount());
    }
  });
});

describe('FileTree select-all shortcut', () => {
  it.each([
    ['ctrl', { ctrlKey: true }],
    ['meta', { metaKey: true }],
  ])('selects every visible row on %s+A', (_label, modifier) => {
    const onSelect = vi.fn();
    const root = renderTree({ onSelect });
    try {
      act(() => {
        container.querySelector('.file-tree-container').dispatchEvent(
          new KeyboardEvent('keydown', { key: 'a', bubbles: true, ...modifier })
        );
      });

      const [ids] = onSelect.mock.calls[0];
      expect(ids.size).toBe(3);
      expect(ids.has(DEEP_ID)).toBe(false);
    } finally {
      act(() => root.unmount());
    }
  });

  it('does not select all when Shift is also held (e.g. Ctrl+Shift+A)', () => {
    const onSelect = vi.fn();
    const root = renderTree({ onSelect });
    try {
      act(() => {
        container.querySelector('.file-tree-container').dispatchEvent(
          new KeyboardEvent('keydown', { key: 'A', bubbles: true, ctrlKey: true, shiftKey: true })
        );
      });

      expect(onSelect).not.toHaveBeenCalled();
    } finally {
      act(() => root.unmount());
    }
  });
});
