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

expect.extend(matchers);

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

  it('renders a password input', () => {
    render(
      <MaskedTextInput />,
    );
    const textbox = screen.getByTestId('input');
    expect(textbox).toBeInTheDocument();
    expect(textbox).toHaveProperty('type', 'password');
  });

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

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

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

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

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

	it('renders the indicator changing when switching visibility state', async () => {
		render(
			<MaskedTextInput
				enhanced
			/>,
		);

		const indicator = screen.getByTestId('indicator');

		await userEvent.click(indicator);
		expect(indicator).toHaveTextContent('Hide');

		await userEvent.click(indicator);
		expect(indicator).toHaveTextContent('Show');
	});

	it('renders the indicator changing when switching visibility state using function ref', async () => {
		render(
			<MaskedTextInput
				enhanced
				ref={vi.fn()}
			/>,
		);

		const indicator = screen.getByTestId('indicator');

		await userEvent.click(indicator);
		expect(indicator).toHaveTextContent('Hide');

		await userEvent.click(indicator);
		expect(indicator).toHaveTextContent('Show');
	});

	it('highlights the indicator when visibility is toggled via keyboard', async () => {
		render(
			<MaskedTextInput
				enhanced
			/>,
		);

		const input = screen.getByTestId('input');
		const indicator = screen.getByTestId('indicator');

		input.focus();
		fireEvent.keyDown(input, { code: 'Space', ctrlKey: true });
		expect(indicator).toHaveClass('text-tertiary');

		fireEvent.keyUp(input, { code: 'Space', ctrlKey: true });
		expect(indicator).toHaveClass('text-primary');
	});

	it('handles keydown events', async () => {
		const onKeyDown = vi.fn();
		render(
			<MaskedTextInput
				enhanced
				onKeyDown={onKeyDown}
			/>,
		);

		const input = screen.getByTestId('input');

		input.focus();
		fireEvent.keyDown(input, { code: 'Space', ctrlKey: true });
		expect(onKeyDown).toBeCalled();
	});

	it('handles keyup events', async () => {
		const onKeyUp = vi.fn();
		render(
			<MaskedTextInput
				enhanced
				onKeyUp={onKeyUp}
			/>,
		);

		const input = screen.getByTestId('input');

		input.focus();
		fireEvent.keyUp(input, { code: 'Space', ctrlKey: true });
		expect(onKeyUp).toBeCalled();
	});

	it('accepts a function ref', () => {
		const ref = vi.fn();

		render(
			<MaskedTextInput
				enhanced
				ref={ref}
			/>,
		);

		expect(ref).toBeCalled();
	});

  describe.each`
    size        | inputClassName | hintClassName | indicatorClassName
    ${'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,
    inputClassName,
    hintClassName,
    indicatorClassName,
  }: {
    size: TextControl.Size,
    inputClassName: string,
    hintClassName: string,
    indicatorClassName: string,
  }) => {
    it('renders input styles', () => {
      render(
        <MaskedTextInput
          size={size}
        />,
      );

      const input = screen.getByTestId('input');
      expect(input).toHaveClass(inputClassName);
    });

    it('renders label styles with indicator', () => {
      render(
        <MaskedTextInput
          size={size}
          label="foo"
          enhanced
        />,
      );
      const label = screen.getByTestId('label');
      expect(label).toHaveClass(hintClassName);
    });

    it('renders hint styles when indicator is present', () => {
      render(
        <MaskedTextInput
          size={size}
          hint="hint"
          enhanced
        />,
      );

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

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

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

  it('renders a block textbox', () => {
    render(
      <MaskedTextInput
        block
      />,
    );

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

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

      const input = screen.getByTestId('input');
      expect(input).toHaveClass(inputClassName);
    });

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

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

  it('handles change events', async () => {
    const onChange = vi.fn().mockImplementationOnce(
      (e: React.ChangeEvent<MaskedTextInputDerivedElement>) => {
        e.preventDefault();
      },
    );
    render(
      <MaskedTextInput
        onChange={onChange}
      />,
    );
    const textbox = screen.getByTestId('input');
    await userEvent.type(textbox, 'foobar');
    expect(onChange).toBeCalled();
  });

  it('handles input events', async () => {
    const onInput = vi.fn().mockImplementationOnce(
      (e: React.SyntheticEvent<MaskedTextInputDerivedElement>) => {
        e.preventDefault();
      },
    );
    render(
      <MaskedTextInput
        onInput={onInput}
      />,
    );
    const textbox = screen.getByTestId('input');
    await userEvent.type(textbox, 'foobar');
    expect(onInput).toBeCalled();
  });
});
