import { Field } from '../field/Field';
import {
  mockMatchMedia,
  mockResizeObserver,
  render,
  RenderResult,
  screen,
  userEvent,
} from '../test-utils';
import DateLookup, { DateLookupProps } from './DateLookup';

import { act } from 'react';

const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTimeAsync });

mockMatchMedia();
mockResizeObserver();

const initialValue = new Date(2000, 0, 1);
const labelProp = 'label';

describe('DateLookup', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(async () => {
    await jest.runOnlyPendingTimersAsync();
    jest.useRealTimers();
    jest.clearAllMocks();
  });

  it('supports `Field` for labeling', () => {
    render(
      <Field label="Date of birth">
        <DateLookup value={initialValue} onChange={() => {}} />
      </Field>,
    );
    const button = screen.getByRole('button', { name: /^Date of birth/ });

    expect(button).toBeInTheDocument();
    expect(button).toHaveAttribute('aria-haspopup');
  });

  it('focuses trigger and opens panel when `Field` label is clicked', async () => {
    const label = 'Date of birth';

    render(
      <Field label={label}>
        <DateLookup value={initialValue} onChange={() => {}} />
      </Field>,
    );

    const button = screen.getByRole('button', { name: /^Date of birth/ });
    await user.click(screen.getByLabelText(label));

    expect(button).toHaveAttribute('aria-expanded', 'true');
    expect(screen.getByRole('button', { name: /next/iu })).toBeInTheDocument();
  });

  it.each([' ', '{Enter}', '{ArrowDown}', '{ArrowUp}', '{ArrowRight}', '{ArrowLeft}'] as const)(
    "opens with '%s' and closes with '{Escape}'",
    async (text: string) => {
      render(<DateLookup value={initialValue} onChange={() => {}} />);

      await user.tab();
      await user.keyboard(text);

      expect(screen.getByRole('button', { name: /next/iu })).toBeInTheDocument();

      await user.keyboard('{Escape}');
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });

      expect(screen.queryByRole('button', { name: /next/iu })).not.toBeInTheDocument();
    },
  );

  const setupAndOpenWithMouse = async (props: Partial<DateLookupProps> = {}) => {
    const view = render(<DateLookup value={initialValue} onChange={() => {}} {...props} />);

    await user.click(screen.getByRole('button'));
    await act(async () => {
      await jest.runOnlyPendingTimersAsync();
    });

    return view;
  };

  it('opens and closes with mouse', async () => {
    await setupAndOpenWithMouse();

    expect(screen.getByRole('button', { name: /next/iu })).toBeInTheDocument();

    const dimmerElement = screen.getByRole('dialog').parentElement?.parentElement;
    if (dimmerElement != null) {
      await user.click(dimmerElement);
    }
    await act(async () => {
      await jest.runOnlyPendingTimersAsync();
    });

    expect(screen.queryByRole('button', { name: /next/iu })).not.toBeInTheDocument();
  });

  describe('in day view', () => {
    it.each([
      ['{ArrowLeft}', -1],
      ['{ArrowRight}', +1],
      ['{ArrowUp}', -7],
      ['{ArrowDown}', +7],
    ])("handles '%s' to step %d day(s)", async (text: string, step: number) => {
      const handleChange = jest.fn();
      await setupAndOpenWithMouse({ onChange: handleChange });

      expect(handleChange).not.toHaveBeenCalled();

      await user.keyboard(text);

      const value = new Date(initialValue);
      value.setDate(initialValue.getDate() + step);
      expect(handleChange).toHaveBeenCalledWith(value);

      await user.keyboard('{Escape}');
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });

      expect(handleChange).toHaveBeenCalledWith(initialValue);
    });
  });

  describe('in year view', () => {
    it.each([
      ['{ArrowLeft}', -1],
      ['{ArrowRight}', +1],
      ['{ArrowUp}', -4],
      ['{ArrowDown}', +4],
    ])("handles '%s' to step %d year(s)", async (text: string, step: number) => {
      const handleChange = jest.fn();
      await setupAndOpenWithMouse({ onChange: handleChange });

      await user.click(screen.getByRole('button', { name: /year view/iu }));
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });

      expect(handleChange).not.toHaveBeenCalled();

      await user.keyboard(text);

      const value = new Date(initialValue);
      value.setFullYear(initialValue.getFullYear() + step);
      expect(handleChange).toHaveBeenCalledWith(value);

      await user.keyboard('{Escape}');
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });

      expect(handleChange).toHaveBeenCalledWith(initialValue);
    });
  });

  describe('in month view', () => {
    it.each([
      ['{ArrowLeft}', -1],
      ['{ArrowRight}', +1],
      ['{ArrowUp}', -4],
      ['{ArrowDown}', +4],
    ])("handles '%s' to step %d month(s)", async (text: string, step: number) => {
      const handleChange = jest.fn();
      await setupAndOpenWithMouse({ onChange: handleChange });

      await user.click(screen.getByRole('button', { name: /year view/iu }));
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });
      await user.keyboard(' ');
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });

      expect(handleChange).not.toHaveBeenCalled();

      await user.keyboard(text);

      const value = new Date(initialValue);
      value.setMonth(initialValue.getMonth() + step);
      expect(handleChange).toHaveBeenCalledWith(value);

      await user.keyboard('{Escape}');
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });

      expect(handleChange).toHaveBeenCalledWith(initialValue);
    });
  });

  it('limits min value', async () => {
    const min = new Date(initialValue);
    min.setDate(min.getDate() - 1);
    const handleChange = jest.fn();
    await setupAndOpenWithMouse({ min, onChange: handleChange });

    await user.keyboard('{ArrowLeft}{ArrowLeft}');

    expect(handleChange).toHaveBeenCalledWith(min);
  });

  it('limits max value', async () => {
    const max = new Date(initialValue);
    max.setDate(max.getDate() + 1);
    const handleChange = jest.fn();
    await setupAndOpenWithMouse({ max, onChange: handleChange });

    await user.keyboard('{ArrowRight}{ArrowRight}');

    expect(handleChange).toHaveBeenCalledWith(max);
  });
});

describe('DateLookup propTypes', () => {
  describe('when the value prop is set to null', () => {
    it('renders without prop type warnings in the console', () => {
      const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

      render(<DateLookup value={null} onChange={jest.fn()} />);

      // eslint-disable-next-line no-console
      expect(console.error).not.toHaveBeenCalledWith(
        expect.stringContaining('Warning: Failed %s type'),
        'prop',
        'The prop `value` is marked as required in `DateLookup`, but its value is `null`.',
        expect.anything(),
      );

      consoleSpy.mockRestore();
    });
  });
});

describe('DateLookup (events)', () => {
  const date = new Date(2018, 11, 27);
  const min = new Date(2018, 11, 26);
  const max = new Date(2018, 11, 28);

  it('focuses on today when value is null', async () => {
    const today = new Date();
    const todayDateLabel = today.toLocaleDateString('en-GB', {
      day: 'numeric',
      month: 'numeric',
      year: 'numeric',
    });
    const expectedHeaderText = today.toLocaleDateString('en-GB', {
      month: 'long',
      year: 'numeric',
    });

    render(<DateLookup value={null} onChange={jest.fn()} />);
    await user.click(screen.getByRole('button'));
    const todayButton = screen.getByLabelText(todayDateLabel);

    expect(screen.getByRole('dialog')).toBeInTheDocument();
    expect(screen.getByText(expectedHeaderText)).toBeInTheDocument();
    expect(todayButton).toHaveClass('today');
    expect(todayButton).toHaveFocus();
  });

  describe('when not clearable', () => {
    let handleChange: jest.Mock;

    const setup = async (overrides = {}) => {
      handleChange = jest.fn();

      return render(
        <>
          {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
          <label id="prioritized-label">Prioritized label</label>
          <DateLookup
            value={date}
            min={min}
            max={max}
            size="lg"
            placeholder="Select date"
            label={labelProp}
            aria-labelledby="prioritized-label"
            disabled={false}
            clearable={false}
            onChange={handleChange}
            {...overrides}
          />
        </>,
      );
    };

    it('switches to years', async () => {
      const view = await setup();
      await openDateLookup();
      await clickDateButton();

      expect(getActiveYearButton(view)).toHaveFocus();

      await closeDateLookup(view);
    });

    it('has aria-label for 20 years', async () => {
      const view = await setup();
      await openDateLookup();
      await clickDateButton();

      expect(getButtonByAriaLabel('next 20 years')).toBeInTheDocument();
      expect(getButtonByAriaLabel('previous 20 years')).toBeInTheDocument();

      await closeDateLookup(view);
    });

    it('switches to months', async () => {
      const view = await setup();
      await openDateLookup();
      await clickDateButton();
      await userEvent.click(getActiveYearButton(view));

      expect(getActiveMonthButton(view)).toHaveFocus();

      await closeDateLookup(view);
    });

    it('has aria label for year', async () => {
      const view = await setup();
      await openDateLookup();
      await clickDateButton();
      await userEvent.click(getActiveYearButton(view));

      expect(getButtonByAriaLabel('next year')).toBeInTheDocument();
      expect(getButtonByAriaLabel('previous year')).toBeInTheDocument();

      await closeDateLookup(view);
    });

    it('switches to days', async () => {
      const view = await setup();
      await openDateLookup();
      await clickDateButton();
      await userEvent.click(getActiveYearButton(view));
      await userEvent.click(getActiveMonthButton(view));

      expect(getActiveDayButton(view)).toHaveFocus();

      await closeDateLookup(view);
    });

    it('has aria label for month', async () => {
      const view = await setup();
      await openDateLookup();
      await clickDateButton();
      await userEvent.click(getActiveYearButton(view));
      await userEvent.click(getActiveMonthButton(view));

      expect(getButtonByAriaLabel('next month')).toBeInTheDocument();
      expect(getButtonByAriaLabel('previous month')).toBeInTheDocument();

      await closeDateLookup(view);
    });

    it('has aria label on selected date', async () => {
      const view = await setup();
      await openDateLookup();
      const d = new Date(2018, 11, 28);
      const newDay = screen.getByText(d.getDate().toString());
      await userEvent.click(newDay);
      await openDateLookup();
      expect(screen.getByRole('button', { name: /selected day/i })).toBeInTheDocument();

      await closeDateLookup(view);
    });

    it('supports custom `aria-labelledby` attribute', async () => {
      const view = await setup();
      expect(screen.getByRole('button', { name: /^Prioritized label/ })).toBeInTheDocument();
      await closeDateLookup(view);
    });

    it('reads our the HTML label as well as the input prefix and the value', async () => {
      const view = await setup();
      expect(
        screen.getByRole('button', { name: 'Prioritized label label 27 December 2018' }),
      ).toBeInTheDocument();
      await closeDateLookup(view);
    });

    it('reads our the HTML label as well as the input prefix and the placeholder', async () => {
      const view = await setup({ value: undefined });
      expect(
        screen.getByRole('button', { name: 'Prioritized label label Select date' }),
      ).toBeInTheDocument();
      await closeDateLookup(view);
    });
  });

  describe('when is clearable', () => {
    const props: DateLookupProps = {
      value: date,
      onChange: jest.fn(),
      clearable: true,
      label: labelProp,
    };

    it(`doesn't show clear button if disable is true`, async () => {
      const view = render(<DateLookup {...props} />);
      expect(getClearButton(view)).toBeInTheDocument();

      view.rerender(<DateLookup {...props} disabled />);

      expect(getClearButton(view)).not.toBeInTheDocument();
    });

    it('when user clicks on clear the focus returns to btn', async () => {
      const view = render(<DateLookup {...props} />);
      await clickClearButton(view);
      expect(getOpenButton(labelProp)).toHaveFocus();
    });

    it('onChange gets called with null when reset button is clicked', async () => {
      const view = render(<DateLookup {...props} />);
      await clickClearButton(view);
      expect(props.onChange).toHaveBeenCalledWith(null);
    });
  });

  const getClearButton = (view: RenderResult) => {
    return view.container.querySelector('.clear-btn');
  };
  const getOpenButton = (label: DateLookupProps['label']) => {
    return screen.getByRole('button', {
      name: new RegExp(String(label), 'i'),
      expanded: false,
    });
  };

  const getDateButton = () => {
    return screen.getByRole('button', {
      name: /Go to 20 year view/i,
    });
  };

  const openDateLookup = async () => userEvent.click(getOpenButton(labelProp));

  const clickDateButton = async () => userEvent.click(getDateButton());

  // Close dateLookup and removes events attached to documents.
  const closeDateLookup = async (view: RenderResult) => userEvent.click(view.container);

  // @ts-expect-error getClearButton returns node
  const clickClearButton = async (view: RenderResult) => userEvent.click(getClearButton(view));

  const getActiveYearButton = (view: RenderResult) => {
    return screen.getByRole('button', {
      name: /selected year/i,
      pressed: true,
    });
  };

  const getActiveMonthButton = (view: RenderResult) => {
    return screen.getByRole('button', {
      name: /selected month/i,
      pressed: true,
    });
  };

  const getActiveDayButton = (view: RenderResult) => {
    return screen.getByRole('button', {
      name: /selected day/i,
      pressed: true,
    });
  };

  const getButtonByAriaLabel = (ariaLabel: string) => {
    return screen.getByRole('button', { name: ariaLabel });
  };
});
