import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';

import { afterEach, describe, expect, it } from 'vitest';

import FileTreePanel from '../FileTreePanel.vue';

const nodes = [
  { name: 'SKILL.md', path: 'SKILL.md', type: 'SKILL' },
  {
    children: [
      { name: 'guide.md', path: 'references/guide.md', type: 'REFERENCE' },
      {
        children: [
          {
            name: 'deep.md',
            path: 'references/nested/deep.md',
            type: 'REFERENCE',
          },
        ],
        name: 'nested',
        path: 'references/nested',
        type: 'DIRECTORY',
      },
    ],
    name: 'references',
    path: 'references',
    type: 'DIRECTORY',
  },
  {
    children: [{ name: 'run.py', path: 'scripts/run.py', type: 'SCRIPT' }],
    name: 'scripts',
    path: 'scripts',
    type: 'DIRECTORY',
  },
];

const wrappers: Array<ReturnType<typeof mount>> = [];

function mountTree(currentPath = 'SKILL.md') {
  const wrapper = mount(FileTreePanel, {
    attachTo: document.body,
    props: { currentPath, nodes },
  });
  wrappers.push(wrapper);
  return wrapper;
}

function treeItem(path: string) {
  return document.querySelector<HTMLElement>(
    `[role="treeitem"][data-tree-path="${path}"]`,
  );
}

function expectFocused(path: string) {
  const target = treeItem(path);
  expect(document.activeElement).toBe(target);
  expect(target?.tabIndex).toBe(0);
  expect(
    document.querySelectorAll('[role="treeitem"][tabindex="0"]'),
  ).toHaveLength(1);
}

afterEach(() => {
  wrappers.splice(0).forEach((wrapper) => wrapper.unmount());
  document.body.innerHTML = '';
});

describe('file tree panel keyboard navigation', () => {
  it('moves through every visible node with ArrowUp, ArrowDown, Home and End', async () => {
    mountTree();
    treeItem('SKILL.md')?.focus();

    await treeItem('SKILL.md')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowDown' }),
    );
    expectFocused('references');

    treeItem('scripts/run.py')?.focus();
    treeItem('scripts/run.py')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'Home' }),
    );
    expectFocused('SKILL.md');

    treeItem('SKILL.md')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'End' }),
    );
    expectFocused('scripts/run.py');

    treeItem('scripts/run.py')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowUp' }),
    );
    expectFocused('scripts');
  });

  it('opens, enters, leaves and closes nested directories with ArrowRight and ArrowLeft', async () => {
    mountTree();
    treeItem('references')?.focus();

    treeItem('references')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }),
    );
    expectFocused('references/guide.md');

    treeItem('references/nested')?.focus();
    treeItem('references/nested')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }),
    );
    expectFocused('references/nested/deep.md');

    treeItem('references/nested/deep.md')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowLeft' }),
    );
    expectFocused('references/nested');

    treeItem('references/nested')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowLeft' }),
    );
    await Promise.resolve();
    expect(treeItem('references/nested')?.getAttribute('aria-expanded')).toBe(
      'false',
    );
    expect(treeItem('references/nested/deep.md')).toBeNull();
    expectFocused('references/nested');

    treeItem('references/nested')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }),
    );
    await Promise.resolve();
    expect(treeItem('references/nested')?.getAttribute('aria-expanded')).toBe(
      'true',
    );
    expectFocused('references/nested');

    treeItem('references/nested')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }),
    );
    expectFocused('references/nested/deep.md');
  });

  it('selects files and directories with Enter or Space', async () => {
    const wrapper = mountTree();

    treeItem('references/guide.md')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }),
    );
    expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({
      path: 'references/guide.md',
    });

    treeItem('references')?.dispatchEvent(
      new KeyboardEvent('keydown', { bubbles: true, key: ' ' }),
    );
    await nextTick();
    expect(treeItem('references')?.getAttribute('aria-expanded')).toBe('false');
    expect(wrapper.emitted('select')).toHaveLength(2);
    expect(wrapper.emitted('select')?.[1]?.[0]).toMatchObject({
      path: 'references',
    });
  });

  it('emits contextual create and file operations from row actions', async () => {
    const wrapper = mountTree();
    const referenceRow = treeItem('references')?.parentElement;

    await referenceRow
      ?.querySelector<HTMLButtonElement>('[aria-label="在此目录新建文件"]')
      ?.click();
    expect(wrapper.emitted('create')?.[0]?.[0]).toMatchObject({
      path: 'references',
    });

    const guideRow = treeItem('references/guide.md')?.parentElement;
    guideRow
      ?.querySelector<HTMLButtonElement>('[aria-label="更多操作"]')
      ?.click();
    await nextTick();
    const download = [...(guideRow?.querySelectorAll('button') || [])].find(
      (button) => button.textContent?.trim() === '下载',
    );
    download?.click();
    expect(wrapper.emitted('download')?.[0]?.[0]).toMatchObject({
      path: 'references/guide.md',
    });
  });

  it('updates the single tab stop when the selected path changes', async () => {
    const wrapper = mountTree();

    await wrapper.setProps({ currentPath: 'scripts/run.py' });

    expect(treeItem('scripts/run.py')?.tabIndex).toBe(0);
    expect(
      document.querySelectorAll('[role="treeitem"][tabindex="0"]'),
    ).toHaveLength(1);
    expect(treeItem('scripts/run.py')?.getAttribute('aria-selected')).toBe(
      'true',
    );
  });
});
