import {cleanup, render, waitFor} from '@testing-library/react';
import {AsyncChildrenApp, TestApp, type UnitSetupProps, runAllSuites} from '@wix/unidriver-common/testing';
import {delay} from '@wix/unidriver-common';
import React, {type ChangeEvent} from 'react';
import {jsdomReactUniDriver} from '../jsdomReactUniDriver';

describe('Common Test Suite', () => {
  runAllSuites({
    setup: async (props: UnitSetupProps) => {
      const {container, unmount} = render(<TestApp {...props} />);
      const driver = jsdomReactUniDriver(container);

      const tearDown = async () => {
        unmount();
        cleanup();
      };
      return {driver, tearDown};
    },
  });
});

describe('Adapter Specific', () => {
  afterEach(() => {
    vi.useRealTimers();
    cleanup();
  });

  describe('click', () => {
    // TODO: check order of fired events
    it('should trigger button events when clicked', async () => {
      const onMouseUp = vi.fn();
      const onClick = vi.fn();
      const onMouseDown = vi.fn();
      const onFocus = vi.fn();

      const {container} = render(
        <button onMouseUp={onMouseUp} onClick={onClick} onMouseDown={onMouseDown} onFocus={onFocus} />,
      );

      const driver = jsdomReactUniDriver(container);
      await driver.$('button').click();
      expect(onClick).toHaveBeenCalledTimes(1);
      expect(onMouseUp).toHaveBeenCalledTimes(1);
      expect(onMouseDown).toHaveBeenCalledTimes(1);
      expect(onFocus).toHaveBeenCalledTimes(1);
    });
    it('should trigger input events in order', async () => {
      const onMouseUp = vi.fn();
      const onClick = vi.fn();
      const onMouseDown = vi.fn();
      const onFocus = vi.fn();
      const onInput = vi.fn();
      const onChange = vi.fn();

      const {container} = render(
        <input
          type="checkbox"
          onMouseUp={onMouseUp}
          onClick={onClick}
          onMouseDown={onMouseDown}
          onFocus={onFocus}
          onInput={onInput}
          onChange={onChange}
        />,
      );

      const driver = jsdomReactUniDriver(container);
      await driver.$('input').click();

      expect(onClick).toHaveBeenCalledTimes(1);
      expect(onMouseUp).toHaveBeenCalledTimes(1);
      expect(onMouseDown).toHaveBeenCalledTimes(1);
      expect(onFocus).toHaveBeenCalledTimes(1);
      expect(onInput).toHaveBeenCalledTimes(1);
      expect(onChange).toHaveBeenCalledTimes(1);
    });

    it('should trigger focus and blur in order when clicking two buttons', async () => {
      const onFocusA = vi.fn();
      const onBlurA = vi.fn();
      const onFocusB = vi.fn();

      const {container} = render(
        <>
          <button id="A" onFocus={onFocusA} onBlur={onBlurA}>
            button A
          </button>
          <button id="B" onFocus={onFocusB}>
            button B
          </button>
        </>,
      );

      const driver = jsdomReactUniDriver(container);

      await driver.$('#A').click();

      expect(onFocusA).toHaveBeenCalledTimes(1);
      expect(onBlurA).toHaveBeenCalledTimes(0);
      expect(onFocusB).toHaveBeenCalledTimes(0);

      await driver.$('#B').click();

      expect(onBlurA).toHaveBeenCalledTimes(1);
      expect(onFocusB).toHaveBeenCalledTimes(1);
    });

    it('should not call onFocus when given disabled button', async () => {
      const onFocusA = vi.fn();

      const {container} = render(
        <button id="A" onFocus={onFocusA} disabled={true}>
          button B
        </button>,
      );

      const driver = jsdomReactUniDriver(container);

      await driver.$('button#A').click();

      expect(onFocusA).toHaveBeenCalledTimes(0);
    });

    it('should work with fake timers', async () => {
      vi.useFakeTimers();
      const {container} = render(<button>button</button>);
      const driver = jsdomReactUniDriver(container);

      // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
      expect(await driver.$('button').click()).toBe(undefined);
    });

    it(`should trigger double click if {type: 'double'} is passed`, async () => {
      const onDoubleClick = vi.fn();

      const {container} = render(<input type="checkbox" onDoubleClick={onDoubleClick} />);

      const driver = jsdomReactUniDriver(container);
      await driver.$('input').click({type: 'double'});

      expect(onDoubleClick).toHaveBeenCalledTimes(1);
    });

    it('should work with pointer-events: none', async () => {
      const {container} = render(<button style={{pointerEvents: 'none'}}>button</button>);
      const driver = jsdomReactUniDriver(container);

      // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
      expect(await driver.$('button').click()).toBe(undefined);
    });
  });

  describe('enterText', () => {
    it('should change input value when entering', async () => {
      const onChange = vi.fn();

      const {container} = render(
        <input
          type="text"
          name="search"
          onChange={(event) => {
            onChange(event.target.value);
          }}
        />,
      );

      const driver = jsdomReactUniDriver(container);

      await driver.$('input').enterText('some keywords');

      const firstLetter: string = onChange.mock.calls.at(0)?.[0];
      expect(firstLetter).toBe('s');

      const value: string = onChange.mock.calls.at(-1)?.[0];
      expect(value).toBe('some keywords');
    });

    it('should change input value when given uncontrolled input', async () => {
      const {container} = render(<input type="text" name="search" />);

      const driver = jsdomReactUniDriver(container);

      const input = driver.$('input');

      await input.enterText('some keywords');

      expect(await input.value()).toBe('some keywords');
    });

    it('should type with a delay if delay prop provided', async () => {
      const {container} = render(<input type="text" name="search" />);

      const driver = jsdomReactUniDriver(container);

      const input = driver.$('input');

      const inputPromise = input.enterText('some keywords', {delay: 100});

      const assertionPromise = delay(300).then(async () => {
        const value = await driver.$('input').value();
        expect(value.length).toBeGreaterThan(0);
        expect(value.length).toBeLessThan('some keywords'.length);
      });

      await inputPromise;
      await assertionPromise;

      expect(await input.value()).toBe('some keywords');
    });
  });

  describe('enterValue', () => {
    // This is actually incorrect behaviour and the reason, why .enterValue() is deprecated
    it('should change input value when entering with single change', async () => {
      const onChange = vi.fn();

      const {container} = render(<input type="text" name="search" onChange={onChange} />);

      const driver = jsdomReactUniDriver(container);

      await driver.$('input').enterValue('some keywords');

      const event: ChangeEvent<HTMLInputElement> = onChange.mock.calls.at(0)?.[0];

      expect(event.target.value).toEqual('some keywords');
      expect(onChange.mock.calls.length).toBe(1);
    });

    it('should change input value when given uncontrolled input', async () => {
      const {container} = render(<input type="text" name="search" />);

      const driver = jsdomReactUniDriver(container);

      const input = driver.$('input');

      await input.enterValue('some keywords');

      expect(await input.value()).toBe('some keywords');
    });

    it('should type with a delay if delay prop provided', async () => {
      const {container} = render(<input type="text" name="search" />);

      const driver = jsdomReactUniDriver(container);

      const input = driver.$('input');

      const inputPromise = input.enterValue('some keywords', {delay: 100});

      const assertionPromise = delay(300).then(async () => {
        const value = await driver.$('input').value();
        expect(value.length).toBeGreaterThan(0);
        expect(value.length).toBeLessThan('some keywords'.length);
      });

      await inputPromise;
      await assertionPromise;

      expect(await input.value()).toBe('some keywords');
    });
  });

  describe('wait', () => {
    it('should throw immediately if element is not found when passed zero timeout', async () => {
      const {container} = render(<div />);
      const driver = jsdomReactUniDriver(container);

      const firstCompletedTask = await Promise.race([
        driver
          .$('button')
          .wait(0)
          .catch(() => {
            return 'wait';
          }),
        new Promise((resolve) =>
          setTimeout(() => {
            resolve('timeout');
          }, 100),
        ),
      ]);

      expect(firstCompletedTask).toBe('wait');
    });
    it('should resolve promise only after the element is in the DOM', async () => {
      const {container, rerender} = render(<div />);
      const driver = jsdomReactUniDriver(container);
      const selector = '[data-testid="test-element"]';

      expect(await driver.$(selector).exists()).toBe(false);

      const waitPromise = driver.$(selector).wait();

      rerender(<div data-testid="test-element" />);

      expect(await driver.$(selector).exists()).toBe(true);

      await expect(waitPromise).resolves.toBe(undefined);
    });
    it('should reject promise if element does not appear in the DOM', async () => {
      const {container} = render(<div />);
      const driver = jsdomReactUniDriver(container);

      await expect(driver.$('button').wait()).rejects.toThrowError();
    });
  });

  describe('$', () => {
    describe('awaited', () => {
      it('should have basic support', async () => {
        const onClick = vi.fn();
        const {container} = render(<button onClick={onClick} />);
        const driver = jsdomReactUniDriver(container);

        const task = await driver
          .$('button')
          .awaited()
          .click()
          .then(() => 'click');

        expect(task).toBe('click');
        expect(onClick).toHaveBeenCalledTimes(1);
      });

      it('should wait timeout for element to show if applied to deep driver', async () => {
        const {container} = render(<div />);
        const driver = jsdomReactUniDriver(container);

        const firstCompletedTask = await Promise.race([
          driver
            .$('div')
            .awaited(100)
            .$('button')
            .click()
            .catch(() => {
              return 'selector';
            }),
          new Promise((resolve) => {
            setTimeout(() => {
              resolve('timeout');
            }, 200);
          }),
        ]);

        expect(firstCompletedTask).toBe('selector');
      });

      it('should override timeout', async () => {
        const {container} = render(<div />);
        const driver = jsdomReactUniDriver(container);

        const firstCompletedTask = await Promise.race([
          driver
            .$('div')
            .awaited(300)
            .$('button')
            .awaited(100)
            .click()
            .catch(() => {
              return 'selector';
            }),
          new Promise((resolve) => {
            setTimeout(() => {
              resolve('timeout');
            }, 200);
          }),
        ]);

        expect(firstCompletedTask).toBe('selector');
      });

      it('should show full selectors path if element not present', async () => {
        const {container} = render(
          <div>
            <section>
              <form>
                <button />
              </form>
            </section>
          </div>,
        );

        const driver = jsdomReactUniDriver(container);

        const clickTaskError = await driver
          .$('div')
          .$('section')
          .$('span')
          .$('div')
          .$('button')
          .awaited(10)
          .click()
          .catch((e: unknown) => {
            return (e as Error).message;
          });

        expect(clickTaskError).toContain('selector: button\r\n');
        expect(clickTaskError).toContain('Failed path: div > section > span\r\n');
        expect(clickTaskError).toContain('Full path: div > section > span > div > button');
      });
    });

    it('should not throw immediately', async () => {
      const {container} = render(<div />);
      const driver = jsdomReactUniDriver(container);

      const completedTask = await Promise.race([
        driver
          .$('button')
          .click()
          .catch(() => {
            return 'selector';
          }),
        new Promise((resolve) => {
          setTimeout(() => {
            resolve('timeout');
          }, 100);
        }),
      ]);

      expect(completedTask).toBe('timeout');
    });

    it('should show full selectors path if element not present', async () => {
      const {container} = render(
        <div>
          <section>
            <form>
              <button />
            </form>
          </section>
        </div>,
      );

      const driver = jsdomReactUniDriver(container);

      const clickTaskError = await driver
        .$('div')
        .$('section')
        .$('span')
        .$('div')
        .$('button')
        .awaited(10)
        .click()
        .catch((e: unknown) => {
          return (e as Error).message;
        });

      expect(clickTaskError).toContain('selector: button\r\n');
      expect(clickTaskError).toContain('Failed path: div > section > span\r\n');
      expect(clickTaskError).toContain('Full path: div > section > span > div > button');
    });

    it('should be able to get an element when app renders element with a delay', async () => {
      const {container} = render(
        <AsyncChildrenApp timeout={20}>
          <div data-testid="delayed-element" />
        </AsyncChildrenApp>,
      );
      const driver = jsdomReactUniDriver(container);

      await waitFor(async () => {
        expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(true);
      });
    });

    it('should return an object which syncs with the DOM', async () => {
      const {container, rerender} = render(<div data-testid="test-element" />);
      const driver = jsdomReactUniDriver(container);

      const element = driver.$('[data-testid="test-element"]');

      expect(await element.exists()).toBe(true);

      rerender(<div />);

      expect(await element.exists()).toBe(false);
    });
  });

  describe('$$', () => {
    it('should return unidriver list', async () => {
      const {container} = render(
        <>
          <div data-testid="test-element" />
          <div data-testid="test-element" />
        </>,
      );
      const driver = jsdomReactUniDriver(container);

      const elementList = driver.$$('[data-testid="test-element"]');

      expect(await elementList.get(0).exists()).toBe(true);
      expect(await elementList.get(1).exists()).toBe(true);
      expect(await elementList.get(2).exists()).toBe(false);
    });

    it('should show full selectors path if element not present', async () => {
      const {container} = render(
        <>
          <div>
            <section>
              <form>
                <button />
              </form>
            </section>
            <section />
          </div>
        </>,
      );

      const driver = jsdomReactUniDriver(container);

      const clickTaskError = await driver
        .$('div')
        .$$('section')
        .at(0)
        .$('span')
        .$('div')
        .$('button')
        .awaited(10)
        .click()
        .catch((e: unknown) => {
          return (e as Error).message;
        });

      expect(clickTaskError).toContain('selector: button\r\n');
      expect(clickTaskError).toContain('Failed path: div > section:nth-child(1) > span\r\n');
      expect(clickTaskError).toContain('Full path: div > section:nth-child(1) > span > div > button');
    });

    it('should be able to get an element(s) when app renders element with a delay', async () => {
      const {container} = render(
        <AsyncChildrenApp timeout={10}>
          <div data-testid="delayed-element" />
          <div data-testid="delayed-element" />
        </AsyncChildrenApp>,
      );
      const driver = jsdomReactUniDriver(container);

      expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(false);

      const elementList = driver.$$('[data-testid="delayed-element"]');

      await waitFor(async () => {
        expect(await elementList.get(0).exists()).toBe(true);
        expect(await elementList.get(1).exists()).toBe(true);
      });
    });

    it(`should sync with the DOM when it's updated`, async () => {
      const {container, rerender} = render(
        <div data-testid="test-element">
          <div data-testid="test-element" />
        </div>,
      );
      const driver = jsdomReactUniDriver(container);

      const elementList = driver.$$('[data-testid="test-element"]');

      expect(await elementList.get(0).exists()).toBe(true);
      expect(await elementList.get(1).exists()).toBe(true);

      rerender(<div />);

      expect(await elementList.get(0).exists()).toBe(false);
      expect(await elementList.get(1).exists()).toBe(false);
    });
  });

  describe('get', () => {
    it('should resolve a promise with a unidriver element', async () => {
      const {container} = render(<div data-testid="test-element" />);
      const driver = jsdomReactUniDriver(container);
      const element = await driver.get('[data-testid="test-element"]');

      expect(await element.exists()).toBe(true);
    });

    it('should be able to get an element when app renders element with a delay', async () => {
      const {container} = render(
        <AsyncChildrenApp timeout={20}>
          <div data-testid="delayed-element" />
        </AsyncChildrenApp>,
      );
      const driver = jsdomReactUniDriver(container);

      expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(false);

      const element = await driver.get('[data-testid="delayed-element"]');

      expect(await element.exists()).toBe(true);
    });

    it('should throw error when element is not found', async () => {
      const {container} = render(<div />);
      const driver = jsdomReactUniDriver(container);

      let error: unknown;

      try {
        await driver.get('[data-testid="test-element"]', {
          timeout: 10,
        });
      } catch (e) {
        error = e;
      }

      expect(error).toBeInstanceOf(Error);
    });

    it('should return an object which syncs with the DOM', async () => {
      const {container, rerender} = render(<div data-testid="test-element" />);
      const driver = jsdomReactUniDriver(container);

      const element = await driver.get('[data-testid="test-element"]');

      expect(await element.exists()).toBe(true);

      rerender(<div />);

      expect(await element.exists()).toBe(false);
    });
  });

  describe('getAll', () => {
    it('should resolve a promise with a unidriver element list', async () => {
      const {container} = render(
        <div data-testid="test-element">
          <div data-testid="test-element" />
        </div>,
      );
      const driver = jsdomReactUniDriver(container);

      const elementList = await driver.getAll('[data-testid="test-element"]');

      expect(await elementList.at(0).exists()).toBe(true);
      expect(await elementList.at(1).exists()).toBe(true);
      expect(await elementList.at(2).exists()).toBe(false);
    });

    it('should be able to get an element(s) when app renders element with a delay', async () => {
      const {container} = render(
        <AsyncChildrenApp>
          <div data-testid="delayed-element" />
          <div data-testid="delayed-element" />
        </AsyncChildrenApp>,
      );
      const driver = jsdomReactUniDriver(container);

      expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(false);

      const elementList = await driver.getAll('[data-testid="delayed-element"]');

      expect(await elementList.at(0).exists()).toBe(true);
      expect(await elementList.at(1).exists()).toBe(true);
    });

    it('should throw error when element is not found', async () => {
      vi.useFakeTimers();
      const {container} = render(<div />);
      const driver = jsdomReactUniDriver(container);
      const getterPromise = driver.getAll('[data-testid="test-element"]');

      await expect(getterPromise).rejects.toThrowError();
    });

    it('should return an object which syncs with the DOM', async () => {
      const {container, rerender} = render(
        <div data-testid="test-element">
          <div data-testid="test-element" />
        </div>,
      );
      const driver = jsdomReactUniDriver(container);

      const elementList = await driver.getAll('[data-testid="test-element"]');

      expect(await elementList.at(0).exists()).toBe(true);
      expect(await elementList.at(1).exists()).toBe(true);

      rerender(<div />);

      expect(await elementList.at(0).exists()).toBe(false);
      expect(await elementList.at(1).exists()).toBe(false);
    });
  });

  it('getNative', async () => {
    const {container} = render(
      <div>
        <span>test</span>
      </div>,
    );
    const driver = jsdomReactUniDriver(container);

    const span = driver.$('span');

    const native: HTMLSpanElement = await span.getNative();

    expect(native.textContent).toBe('test');
  });
});
