import * as React from 'react';
import {
  cleanup,
  render,
  screen,
} from '@testing-library/react';
import { TextControl } from '@tesseract-design/web-base';
import userEvent from '@testing-library/user-event';
import {
  afterEach,
  expect,
  vi,
  describe,
  it,
} from 'vitest';
import matchers from '@testing-library/jest-dom/matchers';
import {
  DropdownSelect,
  DropdownSelectDerivedElement,
} from '.';

expect.extend(matchers);

describe('DropdownSelect', () => {
  afterEach(() => {
    cleanup();
  });

  it('renders a combobox', () => {
    render(<DropdownSelect />);
    const combobox = screen.getByRole('combobox');
    expect(combobox).toBeInTheDocument();
  });

  it('renders a border', () => {
    render(
      <DropdownSelect
        border
      />,
    );
    const border = screen.getByTestId('border');
    expect(border).toBeInTheDocument();
  });

  it('renders a label', () => {
    render(
      <DropdownSelect
        label="foo"
      />,
    );
    const combobox = screen.getByLabelText('foo');
    expect(combobox).toBeInTheDocument();
    const label = screen.getByTestId('label');
    expect(label).toHaveTextContent('foo');
  });

  it('renders a hidden label', () => {
    render(
      <DropdownSelect
        label="foo"
        hiddenLabel
      />,
    );
    const combobox = screen.getByLabelText('foo');
    expect(combobox).toBeInTheDocument();
    const label = screen.getByTestId('label');
    expect(label).toHaveClass('sr-only');
  });

  it('renders a hint', () => {
    render(
      <DropdownSelect
        hint="foo"
      />,
    );
    const hint = screen.getByTestId('hint');
    expect(hint).toBeInTheDocument();
  });

  it('renders an indicator', () => {
    render(
      <DropdownSelect />,
    );
    const indicator = screen.getByTestId('indicator');
    expect(indicator).toBeInTheDocument();
  });

  it('render options with implicit values', () => {
    render(
      <DropdownSelect>
        <option>foo</option>
        <option>bar</option>
      </DropdownSelect>,
    );
    const combobox = screen.getByRole('combobox');
    expect(combobox.children).toHaveLength(2);
  });

  it('renders valid options', () => {
    render(
      <DropdownSelect>
        <option value="foo">foo</option>
        <option value="bar">bar</option>
      </DropdownSelect>,
    );
    const combobox = screen.getByRole('combobox');
    expect(combobox.children).toHaveLength(2);
  });

  it('renders option groups', () => {
    render(
      <DropdownSelect>
        <optgroup label="foo">
          <option value="baz">baz</option>
        </optgroup>
        <optgroup label="bar">
          <option value="quux">quux</option>
          <option value="quuux">quuux</option>
        </optgroup>
      </DropdownSelect>,
    );
    const combobox = screen.getByRole('combobox');
    expect(combobox.children).toHaveLength(2);
    expect(combobox.children[0].children).toHaveLength(1);
    expect(combobox.children[1].children).toHaveLength(2);
  });

  describe.each`
    size        | inputClassNames | hintClassNames | indicatorClassNames
    ${'small'}  | ${'h-10'}       | ${'pr-10'}     | ${'w-10'}
    ${'medium'} | ${'h-12'}       | ${'pr-12'}     | ${'w-12'}
    ${'large'}  | ${'h-16'}       | ${'pr-16'}     | ${'w-16'}
  `('on $size size', ({
    size,
    inputClassNames,
    hintClassNames,
    indicatorClassNames,
  }: {
    size: TextControl.Size,
    inputClassNames: string,
    hintClassNames: string,
    indicatorClassNames: string,
  }) => {
    it('renders input styles', () => {
      render(
        <DropdownSelect
          size={size}
        />,
      );

      const combobox = screen.getByRole('combobox');
      expect(combobox).toHaveClass(inputClassNames);
    });

    it('renders hint styles with indicator', () => {
      render(
        <DropdownSelect
          size={size}
          hint="hint"
        />,
      );

      const hint = screen.getByTestId('hint');
      expect(hint).toHaveClass(hintClassNames);
    });

    it('renders indicator styles', () => {
      render(
        <DropdownSelect
          size={size}
        />,
      );

      const indicator = screen.getByTestId('indicator');
      expect(indicator).toHaveClass(indicatorClassNames);
    });

    it('renders indicator styles for label', () => {
      render(
        <DropdownSelect
          size={size}
          label="label"
        />,
      );

      const label = screen.getByTestId('label');
      expect(label).toHaveClass(hintClassNames);
    });
  });

  it('renders a block input', () => {
    render(
      <DropdownSelect
        block
      />,
    );

    const base = screen.getByTestId('base');
    expect(base).toHaveClass('block');
  });

  describe.each`
    variant        | inputClassNames  | hintClassNames
    ${'default'}   | ${'pl-4'}        | ${'bottom-0 pl-4 pb-1'} 
    ${'alternate'} | ${'pl-1.5 pt-4'} | ${'top-0.5'}
  `('on $variant variant', ({
    variant,
    inputClassNames,
    hintClassNames,
  }: {
    variant: TextControl.Variant,
    inputClassNames: string,
    hintClassNames: string,
  }) => {
    it('renders input styles', () => {
      render(
        <DropdownSelect
          variant={variant}
        />,
      );

      const combobox = screen.getByRole('combobox');
      expect(combobox).toHaveClass(inputClassNames);
    });

    it('renders hint styles', () => {
      render(
        <DropdownSelect
          variant={variant}
          hint="hint"
        />,
      );

      const hint = screen.getByTestId('hint');
      expect(hint).toHaveClass(hintClassNames);
    });
  });

  it('handles change events', async () => {
    const onChange = vi.fn().mockImplementationOnce(
      (e: React.ChangeEvent<DropdownSelectDerivedElement>) => {
        e.preventDefault();
      },
    );

    render(
      <DropdownSelect
        onChange={onChange}
      >
        <option value="foo">foo</option>
        <option value="bar">bar</option>
      </DropdownSelect>,
    );
    const combobox: HTMLSelectElement = screen.getByRole('combobox');
    const [, secondOption] = screen.getAllByRole('option');
    await userEvent.selectOptions(combobox, secondOption);
    expect(onChange).toBeCalled();
  });
});
