import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { expect, fn, type Mock, screen, userEvent, within, waitFor } 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';
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,
  args: {
    onFilterChange: fn() satisfies Mock,
    onChange: fn() satisfies Mock,
    onClose: fn() satisfies Mock,
    onOpen: fn() satisfies Mock,
  },
  argTypes: {
    parentId: {
      table: {
        category: 'WDS internal',
      },
    },
  },
  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} />,
  },
  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 waitFor(async () => 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 waitFor(async () => 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',
  },
  {
    code: 'ÅLD',
    name: 'Ålandian peso',
    countries: ['Ålandia'],
  },
];

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',
} satisfies Story<Currency>['args'];

export const Currencies: Story<Currency> = {
  args: CurrenciesArgs,
  play: async ({ step }) => {
    await step('Tab to the combobox', async () => {
      await userEvent.tab();
    });

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

    await step('Check if options are displayed', async () => {
      await waitFor(async () => expect(await screen.findAllByRole('option')).toHaveLength(9));
      await waitFor(async () => expect(screen.getByText(/^Can’t find it?/u)).toBeInTheDocument());
    });

    await step('Type "huf" in the combobox', async () => {
      // this is workaround for Chromatic: Firefox and Edge tests failing.
      // Unclear on a proper solution
      await wait(750);
      const input = await screen.findByRole('combobox');
      await userEvent.type(input, 'huf');
    });

    await step('Check if no options are displayed', async () => {
      // this is workaround for Chromatic: Firefox and Edge tests failing.
      // Unclear on a proper solution
      await wait(750);
      await expect(await screen.findByText(/^No results found/u)).toBeInTheDocument();
      await expect(await screen.findByText(/^It’s not possible use HUF yet./u)).toBeInTheDocument();
      await waitFor(
        async () => {
          await expect(screen.queryAllByRole('option')).toHaveLength(0);
        },
        { timeout: 1500 },
      );
    });

    await step('Remove last two characters', async () => {
      await wait(500);
      const input = await screen.findByRole('combobox');
      await userEvent.type(input, '{Backspace}{Backspace}');
    });

    await step('Check if two options are displayed', async () => {
      await waitFor(async () =>
        expect(within(screen.getByRole('listbox')).queryAllByRole('option')).toHaveLength(2),
      );
    });

    await step('Type "e" in the combobox', async () => {
      const input = await screen.findByRole('combobox');
      await userEvent.type(input, '{Backspace}e');
    });

    await step('Check if aria-activedescendant is not set', async () => {
      const input = screen.getByRole('combobox');
      await waitFor(async () => expect(input).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, args }) => {
    const canvas = within(canvasElement);

    await step('Open the combobox', async () => {
      const triggerButton = canvas.getByRole('combobox');
      await userEvent.click(triggerButton);
      await wait(500);
      await userEvent.unhover(triggerButton);
      await expect(args.onOpen).toHaveBeenCalledOnce();
    });

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

    await step('Check if selected options are displayed', async () => {
      const triggerButton = document.querySelector('button[role="combobox"]');
      await waitFor(async () => expect(triggerButton).toHaveTextContent('USD, EUR'));
    });
  },
};

export const WithSelectAll: Story<Currency, true> = {
  args: {
    ...MultipleCurrencies.args,
  },
  render: function Render(args) {
    const [selectedItems, setSelectedItems] = useState<Currency[]>([]);

    const allSelected = (items: Currency[]) => {
      const selectedSet = new Set(selectedItems);
      return items.every((item) => selectedSet.has(item));
    };

    const toggleItems = (items: Currency[]) => {
      setSelectedItems((currentItems) =>
        allSelected(items)
          ? currentItems.filter((item) => !items.includes(item))
          : [...new Set([...currentItems, ...items])],
      );
    };

    return (
      <SelectInput
        {...args}
        items={[
          {
            type: 'group',
            label: 'Popular currencies',
            options: popularCurrencies.map((currency) => currencyOption(currency)),
            action: {
              label: allSelected(popularCurrencies) ? 'Deselect all' : 'Select all',
              onClick: () => {
                toggleItems(popularCurrencies);
              },
            },
          },
          {
            type: 'group',
            label: 'Other currencies',
            options: otherCurrencies.map((currency) => currencyOption(currency)),
            action: {
              label: allSelected(otherCurrencies) ? 'Deselect all' : 'Select all',
              onClick: () => {
                toggleItems(otherCurrencies);
              },
            },
          },
        ]}
        value={selectedItems}
        onChange={(currencies) => {
          setSelectedItems(currencies);
        }}
        onClear={() => {
          setSelectedItems([]);
        }}
      />
    );
  },
};

export const WithClear: Story<Currency> = {
  args: {
    ...CurrenciesArgs,
    onClear: fn() satisfies Mock,
  },
  play: async ({ step }) => {
    await step('Has clear button', async () => {
      const clearBtn = await screen.findByRole('button', { name: 'Clear' });
      await expect(clearBtn).toBeInTheDocument();
    });
  },
};

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>
    ),
  },
  play: async ({ canvasElement, step, args }) => {
    const canvas = within(canvasElement);

    await step('Open the combobox', async () => {
      const triggerButton = canvas.getByRole('combobox');
      await userEvent.click(triggerButton);
      await expect(args.onOpen).toHaveBeenCalledOnce();
    });
  },
};

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',
  },
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Open the combobox', async () => {
      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 v2 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 v2 onClick={() => setOpen(true)}>
            Open modal
          </Button>
          <Modal open={open} body={<Story />} onClose={() => setOpen(false)} />
        </>
      );
    },
  ],
};

interface Country {
  code: string;
  name: string;
}

const countries: Country[] = [
  { code: 'US', name: 'United States' },
  { code: 'GB', name: 'United Kingdom' },
  { code: 'CA', name: 'Canada' },
  { code: 'AU', name: 'Australia' },
  { code: 'DE', name: 'Germany' },
  { code: 'FR', name: 'France' },
  { code: 'JP', name: 'Japan' },
  { code: 'BR', name: 'Brazil' },
  { code: 'IN', name: 'India' },
  { code: 'CN', name: 'China' },
  { code: 'IT', name: 'Italy' },
  { code: 'ES', name: 'Spain' },
  { code: 'NL', name: 'Netherlands' },
  { code: 'CH', name: 'Switzerland' },
  { code: 'SE', name: 'Sweden' },
  { code: 'AL', name: 'Ålandia' },
];

function countryOption(country: Country) {
  return {
    type: 'option',
    value: country.code,
    filterMatchers: [country.code, country.name],
  } satisfies SelectInputItem;
}

export const WithAutocomplete: Story<string> = {
  args: {
    name: 'country',
    autocomplete: 'country-name',
    placeholder: 'Select your country',
    items: countries.map(countryOption),
    renderValue: (countryCode, withinTrigger) => {
      const country = countries.find((c) => c.code === countryCode);
      return (
        <SelectInputOptionContent
          title={withinTrigger ? countryCode : country?.name || countryCode}
          note={withinTrigger ? undefined : countryCode}
          icon={<Flag code={countryCode} intrinsicSize={24} />}
        />
      );
    },
    filterable: true,
    filterPlaceholder: 'Type a country name',
    size: 'lg',
  },
  render: function Render({ onChange, onClear, ...args }) {
    const [selectedCountry, setSelectedCountry] = useState<string | undefined>(undefined);

    return (
      <div>
        <form
          method="post"
          onSubmit={(e) => {
            e.preventDefault();
            console.log(
              `Form submitted with country: ${selectedCountry}. This saves data for browser autocomplete!`,
            );
          }}
        >
          <div>
            <label htmlFor="country-select" className="block text-sm font-medium mb-2">
              Country Selection with Autocomplete:
            </label>
            <SelectInput
              {...args}
              id="country-select"
              value={selectedCountry}
              onChange={(country) => {
                setSelectedCountry(country);
                onChange?.(country);
                console.log('Country selected via SelectInput:', country);
              }}
              onClear={() => {
                setSelectedCountry(undefined);
                onClear?.();
              }}
            />
          </div>

          <Button type="submit" v2 className="m-t-2" data-testid="submit-btn">
            Submit Form
          </Button>
        </form>
      </div>
    );
  },
};

interface CountryWithCurrency extends Country {
  keywords: string[];
}

const countriesWithCurrency: CountryWithCurrency[] = [
  { code: 'AD', name: 'Andorra', keywords: ['united states dollar'] },
  { code: 'DE', name: 'Germany', keywords: ['EUR'] },
  { code: 'US', name: 'United States', keywords: ['USD'] },
  { code: 'ZM', name: 'Zambia', keywords: ['USD', 'united states dollar'] },
];

function countryWithCurrencyOption(country: CountryWithCurrency) {
  return {
    type: 'option',
    value: country,
    filterMatchers: [country.name, country.code, ...country.keywords],
  } satisfies SelectInputItem<CountryWithCurrency>;
}

export const WithCustomSearchResultSorting: Story<CountryWithCurrency> = {
  args: {
    items: countriesWithCurrency.map(countryWithCurrencyOption),
    compareValues: 'code',
    renderValue: (country) => (
      <SelectInputOptionContent
        title={country.name}
        icon={<Flag code={country.code} intrinsicSize={24} />}
      />
    ),
    value: countriesWithCurrency[0],
    filterable: true,
    filterPlaceholder: 'Type a currency / country',
    sortFilteredOptions: (a, b, searchQuery) => {
      const normalizedQuery = searchQuery.toLowerCase();
      const nameA = a.value.name.toLowerCase();
      const nameB = b.value.name.toLowerCase();

      // Prioritize countries where name contains the search query
      const aNameMatch = nameA.includes(normalizedQuery);
      const bNameMatch = nameB.includes(normalizedQuery);

      if (aNameMatch && !bNameMatch) return -1;
      if (!aNameMatch && bNameMatch) return 1;

      // Then sort alphabetically
      return nameA.localeCompare(nameB);
    },
    size: 'lg',
  } satisfies Story<CountryWithCurrency>['args'],
  decorators: [
    (Story) => (
      <div>
        <p className="m-b-3 np-text-body-default" style={{ maxWidth: '600px' }}>
          This example demonstrates custom sorting with the <code>sortFilteredOptions</code> prop.
          <br />
          <br />
          Try searching for &quot;united&quot; - Without the custom sorter, it would just be the
          provided options order.
          <br />
          With a customer sorter function in this story, the countries with &quot;United&quot; in
          their name appear first, followed by countries that only match via keywords.
        </p>
        <Story />
      </div>
    ),
  ],
};
