import type { Meta, StoryObj } from '@storybook/react';
import { expect, fn, type Mock, screen, userEvent, within } from '@storybook/test';
import { Calendar, ChevronDown } from '@transferwise/icons';
import { Flag } from '@wise/art';
import { clsx } from 'clsx';
import { useState } from 'react';

import Button from '../button/Button';
import { getMonthNames } from '../common/dateUtils';
import Drawer from '../drawer';
import { Field } from '../field/Field';
import Modal from '../modal';
import { wait } from '../test-utils/wait';
import {
  SelectInput,
  type SelectInputItem,
  SelectInputOptionContent,
  type SelectInputProps,
  SelectInputTriggerButton,
} from './SelectInput';

const meta = {
  title: 'Forms/SelectInput',
  component: SelectInput,
  tags: ['autodocs'],
  parameters: { actions: { argTypesRegex: '' } },
} satisfies Meta<typeof SelectInput>;
export default meta;

type Story<T, M extends boolean = false> = StoryObj<SelectInputProps<T, M>>;

interface Month {
  id: number;
  name: string;
}

const months: Month[] = getMonthNames('en-US').map((name, index) => ({
  id: index + 1,
  name,
}));

export const Months: Story<Month | null> = {
  args: {
    placeholder: 'Month',
    items: months.map((month) => ({
      type: 'option',
      value: month,
    })),
    renderValue: (month) => <SelectInputOptionContent title={month.name} />,
    onFilterChange: fn() satisfies Mock,
    onChange: fn() satisfies Mock,
    onClose: fn() satisfies Mock,
    onClear: fn() satisfies Mock,
  },
  render: function Render({ onChange, onClear, ...args }) {
    const [selectedMonth, setSelectedMonth] = useState<Month | null>(null);

    return (
      <SelectInput
        {...args}
        value={selectedMonth}
        onChange={(month) => {
          setSelectedMonth(month);
          onChange?.(month);
        }}
        onClear={() => {
          setSelectedMonth(null);
          onClear?.();
        }}
      />
    );
  },
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('renders placeholder', async () => {
      const triggerButton = canvas.getByRole('combobox');
      await expect(triggerButton).toHaveTextContent('Month');
    });

    await step('selects option via mouse', async () => {
      const triggerButton = canvas.getByRole('combobox');

      await userEvent.click(triggerButton);
      await userEvent.unhover(triggerButton);

      const option = within(screen.getByRole('listbox')).getByRole('option', {
        name: 'May',
      });
      await userEvent.click(option);

      await expect(triggerButton).toHaveTextContent('May');
    });
  },
};

interface Currency {
  code: string;
  name: string;
  countries?: string[];
}

const popularCurrencies: Currency[] = [
  {
    code: 'USD',
    name: 'United States dollar',
    countries: ['Hong Kong', 'Saudi Arabia'],
  },
  {
    code: 'EUR',
    name: 'Euro',
    countries: ['Spain', 'Germany', 'France', 'Austria', 'Estonia'],
  },
  {
    code: 'GBP',
    name: 'British pound',
    countries: ['England', 'Scotland', 'Wales'],
  },
];

const otherCurrencies: Currency[] = [
  {
    code: 'CAD',
    name: 'Canadian dollar',
    countries: ['Canada'],
  },
  {
    code: 'AUD',
    name: 'Australian dollar',
  },
];

const allCurrencies: Currency[] = [...popularCurrencies, ...otherCurrencies].sort((a, b) =>
  a.code.localeCompare(b.code),
);

function currencyOption(currency: Currency) {
  return {
    type: 'option',
    value: currency,
    filterMatchers: [currency.code, currency.name, ...(currency.countries ?? [])],
  } satisfies SelectInputItem<Currency>;
}

const CurrenciesArgs = {
  items: [
    {
      type: 'group',
      label: 'Popular currencies',
      options: popularCurrencies.map((currency) => currencyOption(currency)),
    },
    {
      type: 'group',
      label: 'All currencies',
      options: allCurrencies.map((currency) => currencyOption(currency)),
    },
  ],
  defaultValue: popularCurrencies[0],
  renderValue: (currency, withinTrigger) => (
    <SelectInputOptionContent
      title={currency.code}
      note={withinTrigger ? undefined : currency.name}
      icon={<Flag code={currency.code} intrinsicSize={24} />}
    />
  ),
  renderFooter: ({ resultsEmpty, queryNormalized }) =>
    resultsEmpty && queryNormalized != null && /^[a-z]{3}$/u.test(queryNormalized) ? (
      <>
        It’s not possible use {queryNormalized.toUpperCase()} yet.{' '}
        <a href="#_" className="np-text-link-default" onClick={(event) => event.preventDefault()}>
          Email me when it’s available.
        </a>
      </>
    ) : (
      <>
        Can’t find it?{' '}
        <a href="#_" className="np-text-link-default" onClick={(event) => event.preventDefault()}>
          Request the currency you need,
        </a>{' '}
        and we’ll notify you once it’s available.
      </>
    ),
  filterable: true,
  filterPlaceholder: 'Type a currency / country',
  size: 'lg',
  onChange: fn() satisfies Mock,
} satisfies Story<Currency>['args'];

export const Currencies: Story<Currency> = {
  args: CurrenciesArgs,
  play: async ({ step }) => {
    await step('filters options via keyboard', async () => {
      await userEvent.tab();
      await userEvent.keyboard(' ');

      await expect(within(screen.getByRole('listbox')).queryAllByRole('option')).toHaveLength(8);
      await expect(screen.getByText(/^Can’t find it?/u)).toBeInTheDocument();

      const input = screen.getByRole('combobox');

      await wait(0); // TODO: Remove
      await userEvent.type(input, 'huf');
      await expect(
        within(screen.getByRole('listbox')).queryByRole('option'),
      ).not.toBeInTheDocument();
      await expect(screen.getByText(/^It’s not possible use HUF yet./u)).toBeInTheDocument();

      await userEvent.type(input, '{Backspace}{Backspace}');
      await expect(within(screen.getByRole('listbox')).queryAllByRole('option')).toHaveLength(2);

      await userEvent.type(input, '{Backspace}e');
      await expect(input).not.toHaveAttribute('aria-activedescendant');
    });
  },
};

export const MultipleCurrencies: Story<Currency, true> = {
  args: {
    ...CurrenciesArgs,
    multiple: true,
    placeholder: 'Choose currencies…',
    defaultValue: [popularCurrencies[0]],
    renderValue: (currency, withinTrigger) =>
      withinTrigger ? (
        currency.code
      ) : (
        <SelectInputOptionContent
          title={currency.code}
          note={currency.name}
          icon={<Flag code={currency.code} intrinsicSize={24} />}
        />
      ),
  },
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('selects multiple options via mouse', async () => {
      const triggerButton = canvas.getByRole('combobox');

      await userEvent.click(triggerButton);
      await userEvent.unhover(triggerButton);

      const option = within(screen.getByRole('listbox')).getAllByRole('option', {
        name: /^EUR/u,
      })[0];
      await userEvent.click(option);

      await expect(triggerButton).toHaveTextContent('USD, EUR');
    });
  },
};

export const CustomTrigger: Story<Month> = {
  args: {
    placeholder: 'Month',
    items: months.map((month) => ({
      type: 'option',
      value: month,
    })),
    renderValue: (month, withinTrigger) =>
      withinTrigger ? month.name : <SelectInputOptionContent title={month.name} />,
    renderTrigger: ({ content, className }) => (
      <SelectInputTriggerButton
        className={clsx(className, 'btn-unstyled np-text-link-large align-items-center')}
        style={{ display: 'inline-flex', columnGap: '0.25rem' }}
      >
        {content}
        <ChevronDown size={16} />
      </SelectInputTriggerButton>
    ),
    onChange: fn() satisfies Mock,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    const triggerButton = canvas.getByRole('combobox');
    await userEvent.click(triggerButton);
  },
};

const quarters = [
  months.slice(0, 3),
  months.slice(3, 6),
  months.slice(6, 9),
  months.slice(9, 12),
] as const;

export const Advanced: Story<Month> = {
  args: {
    placeholder: 'Month',
    items: quarters
      .flatMap<SelectInputItem<Month>>((quarterMonths, quarterIndex) => [
        {
          type: 'group',
          label: `Quarter #${quarterIndex + 1}`,
          options: quarterMonths.map((month, monthIndex) => ({
            type: 'option',
            value: month,
            filterMatchers: [month.name],
            disabled: monthIndex % 6 === 2,
          })),
        },
        { type: 'separator' },
      ])
      .slice(0, -1),
    renderValue: (month, withinTrigger) => (
      <SelectInputOptionContent
        title={month.name}
        note="Note"
        description={withinTrigger ? undefined : `Month #${month.id}`}
        icon={<Calendar size={24} />}
      />
    ),
    filterable: true,
    filterPlaceholder: 'Type a month’s name',
    onChange: fn() satisfies Mock,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    const triggerButton = canvas.getByRole('combobox');
    await userEvent.click(triggerButton);
  },
};

export const ManyItems: Story<string, true> = {
  args: {
    multiple: true,
    items: Array.from({ length: 1000 }, (_, index) => ({
      type: 'option',
      value: String(index + 1),
    })),
    renderValue: (value, withinTrigger) =>
      withinTrigger ? (
        value
      ) : (
        <SelectInputOptionContent
          title={value}
          description={Number(value) % 10 === 0 ? 'Divisible by 10' : undefined}
        />
      ),
    filterable: true,
  },
};

export const WithinField = {
  args: Months.args,
  decorators: [
    (Story) => (
      <Field message="Something went wrong" sentiment="negative" label="Month">
        <Story />
      </Field>
    ),
  ],
} satisfies Story<Month | null>;

export const WithinDrawer: Story<Currency> = {
  args: CurrenciesArgs,
  decorators: [
    (Story, context) => {
      const [open, setOpen] = useState(context.viewMode === 'story');
      return (
        <>
          <Button onClick={() => setOpen(true)}>Open drawer</Button>
          <Drawer
            open={open}
            headerTitle="This is the title of the drawer"
            onClose={() => setOpen(false)}
          >
            <Story />
          </Drawer>
        </>
      );
    },
  ],
};

export const WithinModal: Story<Currency> = {
  args: CurrenciesArgs,
  decorators: [
    (Story, context) => {
      const [open, setOpen] = useState(context.viewMode === 'story');
      return (
        <>
          <Button onClick={() => setOpen(true)}>Open modal</Button>
          <Modal open={open} body={<Story />} onClose={() => setOpen(false)} />
        </>
      );
    },
  ],
};
