import { screen } from '@testing-library/react';

import {
  findVisSelectorTabs,
  mockConsoleMethod,
  renderApp,
} from '../test-utils';
import { NexusVis } from '../vis-packs/nexus/visualizations';

test('visualize NXdata group with "spectrum" interpretation', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/spectrum');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);

  await expect(
    screen.findByRole('figure', { name: 'twoD_spectrum (arb. units)' }) // signal name + `units` attribute
  ).resolves.toBeVisible();
});

test('visualize NXdata group with "image" interpretation', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/image');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'Interference fringes' }) // `long_name` attribute
  ).resolves.toBeVisible();
});

test('visualize NXdata group with 2D signal', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/nx_process/nx_data');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(2);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);
  expect(tabs[1]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'NeXus 2D' }) // `title` dataset
  ).resolves.toBeVisible();
});

test('visualize NXdata group with 1D signal and two 1D axes of same length', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/scatter');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxScatter);

  await expect(
    screen.findByRole('figure', { name: 'scatter_data' })
  ).resolves.toBeVisible();
});

test('visualize NXentry group with relative path to 2D default signal', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(2);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);
  expect(tabs[1]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'NeXus 2D' }) // `title` dataset
  ).resolves.toBeVisible();
});

test('visualize NXentry group with absolute path to 2D default signal', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/nx_process/absolute_default_path');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(2);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);
  expect(tabs[1]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'NeXus 2D' }) // `title` dataset
  ).resolves.toBeVisible();
});

test('visualize NXentry group with old-style signal', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/old-style');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(2);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);
  expect(tabs[1]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'twoD' }) // name of dataset with `signal` attribute
  ).resolves.toBeVisible();
});

test('visualize NXroot group with 2D default signal', async () => {
  await renderApp();

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(2);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);
  expect(tabs[1]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'NeXus 2D' }) // `title` dataset
  ).resolves.toBeVisible();
});

test('visualize NXdata group with 2D complex signal', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/complex');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(2);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);
  expect(tabs[1]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'twoD_complex (amplitude)' }) // signal name + complex visualization type
  ).resolves.toBeVisible();
});

test('visualize NXdata group with 2D complex signal and "spectrum" interpretation', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/complex_spectrum');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);

  await expect(
    screen.findByRole('figure', { name: 'twoD_complex' }) // signal name (complex vis type is displayed as ordinate label)
  ).resolves.toBeVisible();
});

test('visualize NXdata group with "rgb-image" interpretation', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/rgb-image');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxRGB);

  await expect(
    screen.findByRole('figure', { name: 'RGB CMY DGW' }) // `long_name` attribute
  ).resolves.toBeVisible();
});

test('follow SILX styles when visualizing NXdata group', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_entry/log_spectrum');

  const logSelectors = await screen.findAllByRole('button', { name: 'Log' });
  expect(logSelectors).toHaveLength(2); // `log_spectrum` requests both axes to be in log scale
});

test('visualize NXentry group with implicit default child NXdata group', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_no_default');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);

  await expect(
    screen.findByRole('figure', { name: 'oneD' }) // signal name of NXdata group "spectrum"
  ).resolves.toBeVisible();
});

test('show error when `default` entity is not found', async () => {
  const { selectExplorerNode } = await renderApp();

  const errorSpy = mockConsoleMethod('error');
  await selectExplorerNode('nexus_malformed/default_not_found');
  await expect(
    screen.findByText('No entity found at /test')
  ).resolves.toBeVisible();

  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
});

test('show error when `signal` entity is not found', async () => {
  const { selectExplorerNode } = await renderApp();

  const errorSpy = mockConsoleMethod('error');
  await selectExplorerNode('nexus_malformed/signal_not_found');
  await expect(
    screen.findByText('Expected "unknown" signal entity to exist')
  ).resolves.toBeVisible();

  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
});

test('show error when `signal` entity is not a dataset', async () => {
  const { selectExplorerNode } = await renderApp();

  const errorSpy = mockConsoleMethod('error');
  await selectExplorerNode('nexus_malformed/signal_not_dataset');
  await expect(
    screen.findByText('Expected "some_group" signal to be a dataset')
  ).resolves.toBeVisible();

  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
});

test('show error when old-style `signal` entity is not a dataset', async () => {
  const { selectExplorerNode } = await renderApp();

  const errorSpy = mockConsoleMethod('error');
  await selectExplorerNode('nexus_malformed/signal_old-style_not_dataset');
  await expect(
    screen.findByText('Expected old-style "some_group" signal to be a dataset')
  ).resolves.toBeVisible();

  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
});

test('show error when `signal` dataset is not array', async () => {
  const { selectExplorerNode } = await renderApp();

  const errorSpy = mockConsoleMethod('error');
  await selectExplorerNode('nexus_malformed/signal_not_array');
  await expect(
    screen.findByText('Expected dataset to have array shape')
  ).resolves.toBeVisible();

  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
});

test('show error when `signal` dataset is not numeric', async () => {
  const { selectExplorerNode } = await renderApp();

  const errorSpy = mockConsoleMethod('error');
  await selectExplorerNode('nexus_malformed/signal_not_numeric');
  await expect(
    screen.findByText('Expected dataset to have numeric or complex type')
  ).resolves.toBeVisible();

  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
});

test('show fallback message when NXdata group has no `signal` attribute', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_malformed/no_signal');

  await expect(
    screen.findByText('No visualization available for this entity.')
  ).resolves.toBeInTheDocument();
});

test('visualize NXdata group with unknown interpretation', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_malformed/interpretation_unknown');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(2); // support check falls back to signal dataset dimensions (4D supports both Image and Spectrum)
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);
  expect(tabs[1]).toHaveTextContent(NexusVis.NxImage);

  await expect(
    screen.findByRole('figure', { name: 'fourD' }) // signal name
  ).resolves.toBeVisible();
});

test('visualize NXdata group with "rgb-image" interpretation but incompatible signal', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_malformed/rgb-image_incompatible');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1); // support check falls back to signal dataset dimensions (1D supports only Spectrum)
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);

  await expect(
    screen.findByRole('figure', { name: 'oneD' }) // signal name
  ).resolves.toBeVisible();
});

test('ignore unknown `SILX_style` options and invalid values', async () => {
  const { selectExplorerNode } = await renderApp();
  await selectExplorerNode('nexus_malformed/silx_style_unknown');

  const errorSpy = mockConsoleMethod('error');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);

  const scaleSelectors = await screen.findAllByRole('button', {
    name: 'Linear', // the scales of both axes remain unchanged
  });
  expect(scaleSelectors).toHaveLength(2);

  expect(errorSpy).not.toHaveBeenCalled(); // no error
  errorSpy.mockRestore();
});

test('warn in console when `SILX_style` attribute is not valid JSON', async () => {
  const { selectExplorerNode } = await renderApp();

  const warningSpy = mockConsoleMethod('warn');
  await selectExplorerNode('nexus_malformed/silx_style_malformed');

  const tabs = await findVisSelectorTabs();
  expect(tabs).toHaveLength(1);
  expect(tabs[0]).toHaveTextContent(NexusVis.NxSpectrum);

  expect(warningSpy).toHaveBeenCalledWith(
    "Malformed 'SILX_style' attribute: {"
  );
  warningSpy.mockRestore();
});

test('cancel and retry slow fetch of NxSpectrum', async () => {
  jest.useFakeTimers('modern');
  const { user, selectExplorerNode } = await renderApp();

  // Select NXdata group with spectrum interpretation and start fetching dataset values
  await selectExplorerNode('resilience/slow_nx_spectrum');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Cancel all fetches at once
  const errorSpy = mockConsoleMethod('error');
  await user.click(await screen.findByRole('button', { name: /Cancel/ }));

  await expect(screen.findByText('Request cancelled')).resolves.toBeVisible();
  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
  errorSpy.mockRestore();

  // Retry all fetches at once
  await user.click(await screen.findByRole('button', { name: /Retry/ }));
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetches succeed
  jest.runAllTimers();

  await expect(screen.findByRole('figure')).resolves.toBeVisible();

  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});

test('cancel and retry slow fetch of NxImage', async () => {
  jest.useFakeTimers('modern');
  const { user, selectExplorerNode } = await renderApp();

  // Select NXdata group with image interpretation and start fetching dataset values
  await selectExplorerNode('resilience/slow_nx_image');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Cancel all fetches at once
  const errorSpy = mockConsoleMethod('error');
  await user.click(await screen.findByRole('button', { name: /Cancel/ }));
  await expect(screen.findByText('Request cancelled')).resolves.toBeVisible();
  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
  errorSpy.mockRestore();

  // Retry all fetches at once
  await user.click(await screen.findByRole('button', { name: /Retry/ }));
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetches succeed
  jest.runAllTimers();
  await expect(screen.findByRole('figure')).resolves.toBeVisible();

  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});

test('retry fetching automatically when re-selecting NxSpectrum', async () => {
  jest.useFakeTimers('modern');
  const { user, selectExplorerNode } = await renderApp();

  // Select NXdata group with spectrum interpretation and start fetching dataset values
  await selectExplorerNode('resilience/slow_nx_spectrum');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Cancel all fetches at once
  const errorSpy = mockConsoleMethod('error');
  await user.click(await screen.findByRole('button', { name: /Cancel/ }));
  await expect(screen.findByText('Request cancelled')).resolves.toBeVisible();
  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
  errorSpy.mockRestore();

  // Switch to other entity with no visualization
  await selectExplorerNode('entities');
  await expect(screen.findByText(/No visualization/)).resolves.toBeVisible();

  // Select dataset again
  await selectExplorerNode('slow_nx_spectrum');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetches succeed
  jest.runAllTimers();
  await expect(screen.findByRole('figure')).resolves.toBeVisible();

  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});

test('retry fetching automatically when re-selecting NxImage', async () => {
  jest.useFakeTimers('modern');
  const { user, selectExplorerNode } = await renderApp();

  // Select NXdata group with image interpretation and start fetching dataset values
  await selectExplorerNode('resilience/slow_nx_image');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Cancel all fetches at once
  const errorSpy = mockConsoleMethod('error');
  await user.click(await screen.findByRole('button', { name: /Cancel/ }));
  await expect(screen.findByText('Request cancelled')).resolves.toBeVisible();
  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
  errorSpy.mockRestore();

  // Switch to other entity with no visualization
  await selectExplorerNode('entities');
  await expect(screen.findByText(/No visualization/)).resolves.toBeVisible();

  // Select dataset again
  await selectExplorerNode('slow_nx_image');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetches succeed
  jest.runAllTimers();
  await expect(screen.findByRole('figure')).resolves.toBeVisible();

  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});

test('retry fetching automatically when selecting other NxSpectrum slice', async () => {
  jest.useFakeTimers('modern');
  const { user, selectExplorerNode } = await renderApp();

  // Select NXdata group with spectrum interpretation and start fetching dataset values
  await selectExplorerNode('resilience/slow_nx_spectrum');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Cancel all fetches at once
  const errorSpy = mockConsoleMethod('error');
  await user.click(await screen.findByRole('button', { name: /Cancel/ }));
  await expect(screen.findByText('Request cancelled')).resolves.toBeVisible();
  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
  errorSpy.mockRestore();

  // Move to other slice to retry fetching automatically
  const d0Slider = screen.getByRole('slider', { name: 'Dimension slider' });
  d0Slider.focus();
  await user.keyboard('{PageUp}');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetches succeed
  jest.runAllTimers();
  await expect(screen.findByRole('figure')).resolves.toBeVisible();

  // Move back to first slice to retry fetching it automatically
  await user.keyboard('{PageDown}');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetch of first slice succeed
  jest.runAllTimers();
  await expect(screen.findByRole('figure')).resolves.toBeVisible();
  d0Slider.blur(); // remove focus to avoid state update after unmount

  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});

test('retry fetching supporting datasets automatically when selecting other NxImage slice', async () => {
  jest.useFakeTimers('modern');
  const { user, selectExplorerNode } = await renderApp();

  // Select NXdata group with image interpretation and start fetching dataset values
  await selectExplorerNode('resilience/slow_nx_image');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Cancel all fetches at once
  const errorSpy = mockConsoleMethod('error');
  await user.click(await screen.findByRole('button', { name: /Cancel/ }));
  await expect(screen.findByText('Request cancelled')).resolves.toBeVisible();
  expect(errorSpy).toHaveBeenCalledTimes(2); // React logs two stack traces
  errorSpy.mockRestore();

  // Move to other slice to fetch new slice and retry fetching supporting datasets automatically
  const d0Slider = screen.getByRole('slider', { name: 'Dimension slider' });
  d0Slider.focus();
  await user.keyboard('{PageUp}');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetches succeed
  jest.runAllTimers();
  await expect(screen.findByRole('figure')).resolves.toBeVisible();

  // Move back to first slice to retry fetching it automatically
  await user.keyboard('{PageDown}');
  await expect(screen.findByText(/Loading data/)).resolves.toBeVisible();

  // Let fetch of first slice succeed
  jest.runAllTimers();
  await expect(screen.findByRole('figure')).resolves.toBeVisible();
  d0Slider.blur(); // remove focus to avoid state update after unmount

  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});
