import {
  AsyncChildrenApp,
  TestApp,
  type TestAppProps,
  type UnitSetupProps,
  runAllSuites,
} from '@wix/unidriver-common/testing';
import React, {act, createElement} from 'react';
import {createRoot} from 'react-dom/client';
import {jsdomReactUniDriver} from '../jsdomReactUniDriver';

const renderTestApp = (element: HTMLElement, props?: TestAppProps) => {
  const comp = createElement(TestApp, props);
  const root = createRoot(element);

  act(() => {
    root.render(comp);
  });

  return () => {
    act(() => {
      root.unmount();
    });
  };
};

const render = (jsx: React.JSX.Element, options = {attachToBody: true}) => {
  const elem = document.createElement('div');
  if (options.attachToBody) {
    document.body.appendChild(elem);
  }
  const root = createRoot(elem);
  act(() => {
    root.render(jsx);
  });

  const rerender = (jsxEl: React.JSX.Element) => {
    act(() => {
      root.render(jsxEl);
    });
  };

  return {container: elem, rerender};
};

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;

describe('Common Test Suite', () => {
  runAllSuites<UnitSetupProps>({
    setup: async (params) => {
      const div = document.createElement('div');
      document.body.appendChild(div);
      const cleanApp = renderTestApp(div, params);
      const driver = jsdomReactUniDriver(div);

      const tearDown = () => {
        cleanApp();
        return Promise.resolve();
      };

      return {driver, tearDown};
    },
  });
});

describe('Adapter Specific', () => {
  beforeEach(() => {
    document.body.innerHTML = '';
  });

  describe('click', () => {
    it('sends event data properly on simulated events when element is not attached to body', async () => {
      const s = vi.fn();
      const {container} = render(<button onClick={s}>bob</button>);
      const driver = jsdomReactUniDriver(container);

      await driver.$('button').click();
      expect(s).toHaveBeenCalledTimes(1);
    });

    it('sends event data properly on simulated events when element is attached to body', async () => {
      const s = vi.fn();
      const {container} = render(<button onClick={s}>bob</button>);
      const driver = jsdomReactUniDriver(container);

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

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

    it('should trigger [mouseDown, mouseUp, click] in this order and with default main-button(0) when clicked', async () => {
      const mouseDown = vi.fn();
      const mouseUp = vi.fn();
      const click = vi.fn();
      const {container} = render(
        <button onMouseDown={mouseDown} onMouseUp={mouseUp} onClick={click}>
          bob
        </button>,
      );
      const driver = jsdomReactUniDriver(container);

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

      expect(mouseDown).toHaveBeenCalledTimes(1);
      expect(mouseUp).toHaveBeenCalledTimes(1);
      expect(click).toHaveBeenCalledTimes(1);
    });

    it('should trigger [focus] on click', async () => {
      const focus = vi.fn();
      const {container} = render(<button onFocus={focus}>bob</button>, {
        attachToBody: false,
      });
      const driver = jsdomReactUniDriver(container);

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

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

    it('should trigger [mousedown, focus, mouseup, click] events on click', async () => {
      // https://jsbin.com/larubagiwu/1/edit?html,js,console,output
      // https://github.com/wix-incubator/unidriver/pull/86#issuecomment-516809527
      const mousedown = vi.fn();
      const focus = vi.fn();
      const mouseup = vi.fn();
      const click = vi.fn();
      const {container} = render(
        <button onMouseDown={mousedown} onFocus={focus} onMouseUp={mouseup} onClick={click}>
          bob
        </button>,
        {attachToBody: false},
      );
      const driver = jsdomReactUniDriver(container);

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

      expect(mousedown).toHaveBeenCalledTimes(1);
      expect(focus).toHaveBeenCalledTimes(1);
      expect(mouseup).toHaveBeenCalledTimes(1);
      expect(click).toHaveBeenCalledTimes(1);
    });
    describe('on input[type=checkbox]', () => {
      it('should trigger [mousedown, focus, mouseup, click, input, change] events', async () => {
        const mousedown = vi.fn();
        const focus = vi.fn();
        const mouseup = vi.fn();
        const click = vi.fn();
        const input = vi.fn();
        const change = vi.fn();
        const {container} = render(
          <input
            type="checkbox"
            onMouseDown={mousedown}
            onFocus={focus}
            onMouseUp={mouseup}
            onClick={click}
            onInput={input}
            onChange={change}
          />,
          {attachToBody: false},
        );
        const driver = jsdomReactUniDriver(container);

        await driver.$('input').click();

        expect(mousedown).toHaveBeenCalledTimes(1);
        expect(focus).toHaveBeenCalledTimes(1);
        expect(mouseup).toHaveBeenCalledTimes(1);
        expect(click).toHaveBeenCalledTimes(1);
        expect(input).toHaveBeenCalledTimes(1);
        expect(change).toHaveBeenCalledTimes(1);
      });

      it('should trigger change event with a target object containing correct `checked` property', async () => {
        const change = vi.fn();
        const {container} = render(<input type="checkbox" onChange={change} />);
        const driver = jsdomReactUniDriver(container);

        await driver.$('input').click();

        expect(change).toHaveBeenCalledExactlyOnceWith(
          expect.objectContaining({
            target: expect.objectContaining({checked: true}),
          }),
        );
      });
    });

    it('should trigger [focusA, blurA, focusB] when clicking two buttons', async () => {
      const focusA = vi.fn();
      const focusB = vi.fn();
      const blurA = vi.fn();
      const {container} = render(
        <div>
          <button id="A" onFocus={focusA} onBlur={blurA}>
            button A
          </button>
          <button id="B" onFocus={focusB}>
            button B
          </button>
        </div>,
      );
      const driver = jsdomReactUniDriver(container);

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

      expect(focusA).toHaveBeenCalledTimes(2); // TODO - should be 1
      expect(blurA).toHaveBeenCalledTimes(2); // TODO - should be 1
      expect(focusB).toHaveBeenCalledTimes(2); // TODO - should be 1
    });

    it('should trigger [focusA, blurA] when clicking enabled and disabled button', async () => {
      const focusA = vi.fn();
      const focusB = vi.fn();
      const blurA = vi.fn();

      const {container} = render(
        <div>
          <button id="A" onFocus={focusA} onBlur={blurA}>
            button A
          </button>
          <button id="B" onFocus={focusB} disabled={true}>
            button B
          </button>
        </div>,
      );

      const driver = jsdomReactUniDriver(container);

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

      expect(focusA).toHaveBeenCalledTimes(2); // TODO - should be 1
      expect(blurA).toHaveBeenCalledTimes(1);
      expect(focusB).not.toHaveBeenCalled();
    });

    it('should trigger blur on active element when clicking an svg', async () => {
      const blurA = vi.fn();
      const {container} = render(
        <div>
          <button id="A" onBlur={blurA}>
            button A
          </button>
          <svg id="B" width="100" height="100">
            <circle cx="50" cy="50" r="40" />
          </svg>
        </div>,
      );
      const driver = jsdomReactUniDriver(container);

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

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

  describe('enterValue', () => {
    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').enterValue('some keywords');

      const value = onChange.mock.calls.at(0)?.[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.enterValue('some keywords');

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

    test.todo('should type with a delay if delay prop provided');
  });

  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 value = onChange.mock.calls.at(0)?.[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');
    });

    test.todo('should type with a delay if delay prop provided');
  });

  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 originalConsoleError = console.error.bind(console);

      vi.spyOn(console, 'error').mockImplementation((...args: any[]) => {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unused-expressions
        args[0]?.includes('Warning: The current testing environment is not configured to support act')
          ? undefined
          : // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
            originalConsoleError(...args);
      });

      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 be able to get an element when app renders element with a delay', async () => {
      const {container} = render(
        <AsyncChildrenApp>
          <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 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 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 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 elements 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 reject a promise if an element is not found', async () => {
      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);
    });
  });
});
