import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
import { useState } from 'react';

import { withVariantConfig } from '../../.storybook/helpers';
import Select, { SelectOptionItem } from './Select';

const meta = {
  title: 'Forms/Select/Tests',
  component: Select,
  tags: ['!autodocs', '!manifest'],
} satisfies Meta<typeof Select>;
export default meta;

type Story = StoryObj<typeof Select>;

const options: SelectOptionItem[] = Array.from({ length: 10 }, (_, i) => ({
  value: i + 1,
  label: `Option ${i + 1}`,
}));

const wait = async (duration = 500) =>
  new Promise<void>((resolve) => {
    setTimeout(resolve, duration);
  });

/**
 * Visual regression story: Select placed at the bottom of the viewport so the dropdown
 * flips to the top when opened. The play function opens the dropdown automatically.
 */
export const DropdownFlipsToTop: Story = {
  render: function Render() {
    const [selected, setSelected] = useState<SelectOptionItem>(options[0]);
    return (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'flex-end',
          minHeight: '50vh',
        }}
      >
        <Select
          id="flip-test-select"
          size="md"
          block
          options={options}
          selected={selected}
          onChange={(option) => {
            if (option && 'value' in option) {
              setSelected(option as SelectOptionItem);
            }
          }}
        />
      </div>
    );
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const trigger = canvas.getByRole('button', { name: /Option 1/i });
    await userEvent.click(trigger);
  },
  ...withVariantConfig(['default']),
};

/**
 * This test insures that there's no unintended page scroll after the dropdown
 * is closed without selecting an option (e.g. clicking outside).
 * It tests 2 scenarios:
 * 1. when the dropdown fits naturally on the screen below the trigger
 * 2. when there's not enough space below the trigger and it renders above it
 */
const generateNoScrollTest = (inverted?: boolean): Story => ({
  render: function Render() {
    const [selected, setSelected] = useState<SelectOptionItem>(options[0]);
    return (
      <div>
        <style>
          {`
            .storybook-container{
              min-height: unset;
              height: unset;
            }
          `}
        </style>
        {inverted ? <p style={{ height: '70vh', background: 'blanchedalmond' }} /> : null}

        <Select
          id="flip-test-select"
          size="md"
          block
          options={options}
          selected={selected}
          onChange={(option) => {
            if (option && 'value' in option) {
              setSelected(option as SelectOptionItem);
            }
          }}
        />

        <p style={{ margin: '1rem 0', height: '150vh', background: 'blanchedalmond' }} />
      </div>
    );
  },
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);
    const trigger = canvas.getByRole('button', { name: /Option 1/i });

    await step('Open the combobox', async () => {
      await userEvent.click(trigger);
    });

    await step('Check if options are displayed', async () => {
      await waitFor(async () =>
        expect((await screen.findAllByRole('option')).length).toBeGreaterThan(0),
      );
    });

    await step('Close the combobox', async () => {
      await userEvent.keyboard('{Escape}');
      await wait();
    });

    await step('Verify the page has not scrolled', async () => {
      const scrollParent = canvasElement.ownerDocument.documentElement;
      await expect(scrollParent.scrollTop).toBe(0);
    });
  },
});

export const NoScrollAfterCloseTop: Story = generateNoScrollTest();
export const NoScrollAfterCloseBottom: Story = generateNoScrollTest(true);
