import {
  fireEvent,
  render,
  screen,
  waitFor,
  within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import App from './App';
import { PREFERENCES_KEY } from './preferences';
import { catalogue, catalogueFetch } from './test/fixtures';

afterEach(() => vi.unstubAllGlobals());

function toolLink(name: string) {
  return screen.getByRole('link', { name: `Open ${name}` });
}

function queryToolLink(name: string) {
  return screen.queryByRole('link', { name: `Open ${name}` });
}

function findToolLink(name = 'PDF Workbench') {
  return screen.findByRole('link', { name: `Open ${name}` });
}

describe('portal UI', () => {
  it('renders compact actionable tiles and persists pin/hide controls', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    render(<App />);
    const launch = await findToolLink();
    const card = screen.getByTestId('app-card-de.add-ideas.pdf-tools');
    expect(within(card).getByRole('heading', { level: 3 })).toHaveTextContent(
      'PDF'
    );
    expect(within(card).getByText('Arrange PDF pages locally.')).toBeVisible();
    expect(
      screen.getByTestId('app-card-de.add-ideas.xslt-tools')
    ).toHaveTextContent('XSLT');
    expect(
      screen.getByTestId('app-card-de.add-ideas.onenote-tools')
    ).toHaveTextContent('OneNote');
    expect(
      screen.queryByText(
        'Page-level PDF operations performed locally in the browser.'
      )
    ).not.toBeInTheDocument();
    expect(
      screen.queryByText(
        'Local processing · no file uploads declared · no telemetry declared'
      )
    ).not.toBeInTheDocument();
    expect(
      screen.getAllByRole('link', { name: 'Source and license' })
    ).toHaveLength(1);
    expect(
      screen.queryByRole('region', { name: 'Pinned' })
    ).not.toBeInTheDocument();
    const showHidden = screen.getByRole('checkbox', {
      name: 'Show hidden (0)',
    });
    expect(showHidden).toBeDisabled();
    expect(showHidden).not.toBeChecked();
    expect(launch).toHaveClass('app-card__launch-link');
    expect(
      new URL(launch.getAttribute('href')!).searchParams.get('toolbox')
    ).toContain('toolbox.catalog.json');
    const actions = within(card).getByRole('group', {
      name: 'Actions for PDF Workbench',
    });
    const info = within(actions).getByRole('button', {
      name: 'More information about PDF Workbench',
    });
    expect(info).toHaveAttribute('aria-haspopup', 'dialog');
    for (const control of within(actions).getAllByRole('button'))
      expect(control.closest('a')).toBeNull();
    expect(launch).not.toContainElement(info);

    await user.click(screen.getByRole('button', { name: 'Pin PDF Workbench' }));
    await waitFor(() =>
      expect(localStorage.getItem(PREFERENCES_KEY)).toContain(
        'de.add-ideas.pdf-tools'
      )
    );
    expect(screen.getByRole('region', { name: 'Pinned' })).toContainElement(
      screen.getByTestId('app-card-de.add-ideas.pdf-tools')
    );
    expect(
      screen.queryByRole('button', { name: /Move PDF Workbench/i })
    ).not.toBeInTheDocument();
    const unpin = screen.getByRole('button', {
      name: 'Unpin PDF Workbench',
    });
    expect(unpin).toHaveAttribute('aria-pressed', 'true');
    expect(unpin).toHaveClass('is-active');
    await waitFor(() => expect(unpin).toHaveFocus());
    expect(
      screen.getByRole('button', { name: 'Reorder PDF Workbench' })
    ).toBeInTheDocument();

    await user.click(
      screen.getByRole('button', { name: 'Hide PDF Workbench' })
    );
    expect(queryToolLink('PDF Workbench')).not.toBeInTheDocument();
    expect(showHidden).toBeEnabled();
    expect(showHidden).toHaveAccessibleName('Show hidden (1)');
    await waitFor(() => expect(showHidden).toHaveFocus());
    fireEvent.click(showHidden);
    expect(toolLink('PDF Workbench')).toBeInTheDocument();
    await user.click(
      screen.getByRole('button', { name: 'Show PDF Workbench' })
    );
    expect(showHidden).toBeDisabled();
    expect(showHidden).not.toBeChecked();
    await waitFor(() =>
      expect(
        screen.getByRole('button', { name: 'Hide PDF Workbench' })
      ).toHaveFocus()
    );
  });

  it('uses the compact Regex presentation for the released tool', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(
        catalogueFetch({
          '/toolbox.catalog.json': Response.json({
            ...catalogue,
            apps: [
              ...catalogue.apps,
              { manifest: './apps/regex/toolbox-app.json', enabled: true },
            ],
          }),
        })
      )
    );
    render(<App />);

    await findToolLink();
    const card = screen.getByTestId('app-card-de.add-ideas.regex-tools');
    expect(within(card).getByRole('heading', { level: 3 })).toHaveTextContent(
      'Regex'
    );
    expect(
      within(card).getByText('Explain and test regex locally.')
    ).toBeVisible();
  });

  it('moves the full disclosure into an accessible modal', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    const { container } = render(<App />);
    await findToolLink();
    const info = screen.getByRole('button', {
      name: 'More information about PDF Workbench',
    });

    await user.click(info);
    const dialog = screen.getByRole('dialog', { name: 'PDF Workbench' });
    expect(dialog).toHaveAttribute('aria-modal', 'true');
    expect(dialog).toHaveTextContent(
      'Page-level PDF operations performed locally in the browser.'
    );
    expect(dialog).toHaveTextContent(
      'Local processing · no file uploads declared · no telemetry declared'
    );
    expect(dialog).toHaveTextContent(
      'Needs Secure context · Web workers · IndexedDB'
    );
    expect(dialog).toHaveTextContent('v0.4.3');
    expect(
      within(dialog).getByRole('button', { name: 'documents' })
    ).toBeInTheDocument();
    expect(
      within(dialog).getByRole('button', { name: '#merge' })
    ).toBeInTheDocument();
    const sourceLink = within(dialog).getByRole('link', {
      name: 'Source and license',
    });
    expect(sourceLink).toHaveAttribute(
      'href',
      'https://git.add-ideas.de/zemion/pdf-tools'
    );
    expect(sourceLink).toHaveAttribute('rel', 'noopener noreferrer');
    expect(
      within(dialog).getByRole('link', { name: 'Open PDF' })
    ).toBeInTheDocument();
    expect(container.querySelector('.site-shell')).toHaveAttribute('inert');
    expect(container.querySelector('.site-shell')).toHaveAttribute(
      'aria-hidden',
      'true'
    );
    expect(document.documentElement).toHaveStyle({ overflow: 'hidden' });

    const close = within(dialog).getByRole('button', {
      name: 'Close information for PDF Workbench',
    });
    expect(close).toHaveFocus();
    within(dialog).getByRole('link', { name: 'Open PDF' }).focus();
    await user.tab();
    expect(close).toHaveFocus();

    await user.keyboard('{Escape}');
    expect(
      screen.queryByRole('dialog', { name: 'PDF Workbench' })
    ).not.toBeInTheDocument();
    expect(info).toHaveFocus();
    expect(container.querySelector('.site-shell')).not.toHaveAttribute('inert');
    expect(document.documentElement).not.toHaveStyle({ overflow: 'hidden' });
  });

  it('keeps search and category controls together and filters with each', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    render(<App />);
    await findToolLink();

    const search = screen.getByRole('searchbox', { name: 'Search tools' });
    const category = screen.getByRole('combobox', {
      name: 'Filter by category',
    });
    expect(search.closest('.tool-controls')).toBe(
      category.closest('.tool-controls')
    );

    await user.type(search, 'xslt');
    expect(toolLink('XSLT Workbench')).toBeInTheDocument();
    expect(queryToolLink('PDF Workbench')).not.toBeInTheDocument();

    await user.clear(search);
    await user.selectOptions(category, 'notes');
    expect(toolLink('OneNote Reader')).toBeInTheDocument();
    expect(queryToolLink('XSLT Workbench')).not.toBeInTheDocument();

    await user.selectOptions(category, '');
    await user.click(
      screen.getByRole('button', {
        name: 'More information about PDF Workbench',
      })
    );
    await user.click(
      within(screen.getByRole('dialog', { name: 'PDF Workbench' })).getByRole(
        'button',
        { name: 'pdf' }
      )
    );
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    expect(category).toHaveValue('pdf');
    expect(toolLink('PDF Workbench')).toBeInTheDocument();
    expect(queryToolLink('OneNote Reader')).not.toBeInTheDocument();
  });

  it('keeps remaining hidden tools visible while editing the hidden set', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    render(<App />);
    await findToolLink();

    await user.click(
      screen.getByRole('button', { name: 'Hide PDF Workbench' })
    );
    const showHidden = screen.getByRole('checkbox', {
      name: 'Show hidden (1)',
    });
    await user.click(showHidden);
    await user.click(
      screen.getByRole('button', { name: 'Hide XSLT Workbench' })
    );
    expect(showHidden).toHaveAccessibleName('Show hidden (2)');
    await user.click(
      screen.getByRole('button', { name: 'Show PDF Workbench' })
    );

    expect(showHidden).toBeChecked();
    expect(showHidden).toHaveAccessibleName('Show hidden (1)');
    expect(toolLink('XSLT Workbench')).toBeInTheDocument();
  });

  it('clears search filters without revealing hidden tools', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    render(<App />);
    await findToolLink();
    await user.click(
      screen.getByRole('button', { name: 'Hide PDF Workbench' })
    );
    await user.type(
      screen.getByRole('searchbox', { name: 'Search tools' }),
      'no tool has this phrase'
    );
    await user.click(screen.getByRole('button', { name: 'Clear filters' }));

    expect(queryToolLink('PDF Workbench')).not.toBeInTheDocument();
    expect(toolLink('XSLT Workbench')).toBeInTheDocument();
    expect(
      screen.getByRole('checkbox', { name: 'Show hidden (1)' })
    ).not.toBeChecked();
  });

  it('lists contextual destinations in the Apps menu', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    render(<App />);
    await findToolLink();
    await user.click(screen.getByRole('button', { name: 'Apps' }));
    const navigation = screen.getByRole('navigation', {
      name: 'Toolbox applications',
    });
    const destination = within(navigation).getByRole('link', {
      name: 'XSLT Workbench',
    });
    expect(
      new URL(destination.getAttribute('href')!).searchParams.get('toolbox')
    ).toContain('toolbox.catalog.json');
  });

  it('uses the shared accessible header identity and control order', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    render(<App />);
    await findToolLink();

    const header = document.querySelector<HTMLElement>('.portal-header')!;
    expect(
      within(header).getByRole('heading', { level: 1, name: 'Toolbox' })
    ).toBeInTheDocument();
    expect(
      within(header).getByText('Local-first browser tools')
    ).toBeInTheDocument();
    expect(
      screen.getByRole('heading', {
        level: 2,
        name: 'Your document tools, in one calm place.',
      })
    ).toBeInTheDocument();
    expect(
      screen.getAllByRole('heading', {
        name: 'Your document tools, in one calm place.',
      })
    ).toHaveLength(1);

    const personalize = within(header).getByRole('button', {
      name: 'Personalize',
    });
    const apps = within(header).getByRole('button', { name: 'Apps' });
    const source = within(header).getByRole('link', {
      name: 'Toolbox source on Gitea',
    });
    const help = within(header).getByRole('button', { name: 'Help' });
    const brand = within(header).getByRole('link', {
      name: 'add·ideas Toolbox',
    });
    const controls = within(header).getByRole('group', {
      name: 'Toolbox controls',
    });
    expect(
      Array.from(controls.children, (element) =>
        element.getAttribute('aria-label')
      )
    ).toEqual(['Help', 'Toolbox source on Gitea', 'Apps', 'Personalize']);
    expect(help.compareDocumentPosition(source)).toBe(
      Node.DOCUMENT_POSITION_FOLLOWING
    );
    expect(source.compareDocumentPosition(apps)).toBe(
      Node.DOCUMENT_POSITION_FOLLOWING
    );
    expect(apps.compareDocumentPosition(personalize)).toBe(
      Node.DOCUMENT_POSITION_FOLLOWING
    );
    expect(
      Array.from(
        header.querySelector('.toolbox-shell__bar')?.children ?? [],
        (element) => element.className
      )
    ).toEqual([
      'toolbox-shell__brand',
      'toolbox-shell__identity',
      'toolbox-shell__controls',
    ]);
    expect(brand.querySelector('img')).toHaveAttribute('src', './favicon.svg');
  });

  it('persists an explicit appearance and accepts cross-tab updates', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    render(<App />);
    await findToolLink();
    const personalize = screen.getByRole('button', { name: 'Personalize' });
    await user.click(personalize);
    expect(
      screen.getByRole('heading', { name: 'Personalize your toolbox' })
    ).toBeInTheDocument();
    await user.click(screen.getByRole('button', { name: 'Dark' }));
    expect(document.documentElement).toHaveAttribute('data-theme', 'dark');
    await waitFor(() =>
      expect(localStorage.getItem(PREFERENCES_KEY)).toContain('"theme":"dark"')
    );
    await user.click(personalize);

    fireEvent(
      window,
      new StorageEvent('storage', {
        key: PREFERENCES_KEY,
        newValue: JSON.stringify({
          version: 1,
          pinned: [],
          order: [
            'de.add-ideas.pdf-tools',
            'de.add-ideas.xslt-tools',
            'de.add-ideas.onenote-tools',
          ],
          hidden: [],
          theme: 'light',
        }),
      })
    );
    expect(document.documentElement).toHaveAttribute('data-theme', 'light');
  });

  it('supports keyboard drag ordering from each tile grip', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    render(<App />);
    await findToolLink();
    const cards = [
      screen.getByTestId('app-card-de.add-ideas.pdf-tools'),
      screen.getByTestId('app-card-de.add-ideas.xslt-tools'),
      screen.getByTestId('app-card-de.add-ideas.onenote-tools'),
    ];
    cards.forEach((card, index) => {
      const rect = {
        x: index * 220,
        y: 0,
        width: 188,
        height: 188,
        top: 0,
        right: index * 220 + 188,
        bottom: 188,
        left: index * 220,
        toJSON: () => ({}),
      } as DOMRect;
      card.getBoundingClientRect = () => rect;
      card.getClientRects = () => [rect] as unknown as DOMRectList;
    });

    const grip = screen.getByRole('button', { name: 'Reorder PDF Workbench' });
    grip.focus();
    fireEvent.keyDown(grip, { key: ' ', code: 'Space' });
    await waitFor(() => expect(grip).toHaveAttribute('aria-pressed', 'true'));
    await new Promise((resolve) => setTimeout(resolve, 0));
    fireEvent.keyDown(document, { key: 'ArrowRight', code: 'ArrowRight' });
    fireEvent.keyDown(document, { key: ' ', code: 'Space' });
    await waitFor(() => {
      const tools = screen.getByRole('region', { name: 'Tools' });
      expect(
        within(tools)
          .getAllByRole('heading', { level: 3 })
          .map((heading) => heading.textContent)
      ).toEqual(['XSLT', 'PDF', 'OneNote']);
    });
    expect(
      JSON.parse(localStorage.getItem(PREFERENCES_KEY) ?? '{}').order
    ).toEqual([
      'de.add-ideas.xslt-tools',
      'de.add-ideas.pdf-tools',
      'de.add-ideas.onenote-tools',
    ]);
  });

  it('filters by clickable tags and clears an active tag', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    render(<App />);
    await findToolLink();
    await user.click(
      screen.getByRole('button', {
        name: 'More information about PDF Workbench',
      })
    );
    const dialog = screen.getByRole('dialog', { name: 'PDF Workbench' });
    await user.click(within(dialog).getByRole('button', { name: '#merge' }));

    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    expect(toolLink('PDF Workbench')).toBeInTheDocument();
    expect(queryToolLink('XSLT Workbench')).not.toBeInTheDocument();
    expect(queryToolLink('OneNote Reader')).not.toBeInTheDocument();

    const activeTag = screen.getByRole('button', {
      name: /#merge.*Clear tag filter/i,
    });
    await user.click(activeTag);
    expect(await findToolLink('XSLT Workbench')).toBeInTheDocument();
    expect(toolLink('OneNote Reader')).toBeInTheDocument();
  });

  it('opens the standard help control and explains tile interaction', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    const { container } = render(<App />);
    await findToolLink();
    const help = screen.getByRole('button', { name: 'Help' });
    await user.click(help);
    const dialog = screen.getByRole('dialog', {
      name: 'Choose and arrange your tools',
    });
    expect(dialog).toHaveTextContent('click anywhere on a tool tile');
    expect(dialog).toHaveTextContent('use the light bulb');
    expect(container.querySelector('.site-shell')).toHaveAttribute('inert');
    expect(container.querySelector('.site-shell')).toHaveAttribute(
      'aria-hidden',
      'true'
    );
    const close = within(dialog).getByRole('button', { name: 'Close help' });
    expect(close).toHaveFocus();
    await user.tab();
    expect(close).toHaveFocus();
    await user.keyboard('{Escape}');
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    expect(container.querySelector('.site-shell')).not.toHaveAttribute('inert');
    expect(help).toHaveFocus();
  });

  it('shows a recoverable catalogue error', async () => {
    const fetchMock = vi
      .fn()
      .mockResolvedValueOnce(new Response('no', { status: 503 }))
      .mockImplementation(catalogueFetch());
    vi.stubGlobal('fetch', fetchMock);
    render(<App />);
    expect(
      await screen.findByRole('heading', { name: 'Toolbox unavailable' })
    ).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
    expect(await findToolLink()).toBeInTheDocument();
  });

  it('shows a dedicated empty state for a valid catalogue', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(
        catalogueFetch({
          '/toolbox.catalog.json': Response.json({ ...catalogue, apps: [] }),
        })
      )
    );
    render(<App />);
    expect(
      await screen.findByRole('heading', { name: 'No tools yet' })
    ).toBeInTheDocument();
  });

  it('hosts compact preferences in the shared Personalize popover', async () => {
    vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
    const user = userEvent.setup();
    const { container } = render(<App />);
    const personalize = screen.getByRole('button', { name: 'Personalize' });

    await user.click(personalize);
    const heading = screen.getByRole('heading', {
      name: 'Personalize your toolbox',
    });
    const popover = screen.getByRole('dialog', { name: 'Personalize Toolbox' });
    expect(popover).toContainElement(heading);
    expect(popover).toContainElement(screen.getByText('On this device'));
    expect(
      within(popover).getByRole('group', { name: 'Appearance' })
    ).toBeInTheDocument();
    expect(
      popover.querySelector('.toolbox-shell__preferences-content')
    ).toBeInTheDocument();
    expect(popover).not.toHaveAttribute('aria-modal');
    expect(container.querySelector('.site-shell')).not.toHaveAttribute('inert');
    expect(screen.getByText(/does not transmit them/i)).toHaveTextContent(
      /Trusted code in another Toolbox app on this site can access same-origin storage/i
    );
    expect(
      screen.getByRole('button', { name: 'Export JSON' })
    ).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: 'Import JSON' })
    ).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Reset' })).toBeInTheDocument();

    await user.keyboard('{Escape}');
    expect(
      screen.queryByRole('dialog', { name: 'Personalize Toolbox' })
    ).not.toBeInTheDocument();
    expect(personalize).toHaveFocus();
  });
});
