import { noop } from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
import { useMockMutationObserver, useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import LazyLoader from '~/lazy_loader';
import waitForPromises from './helpers/wait_for_promises';

const execImmediately = callback => {
  callback();
};

const TEST_PATH = `${TEST_HOST}/img/testimg.png`;

describe('LazyLoader', () => {
  let lazyLoader = null;

  const { trigger: triggerMutation } = useMockMutationObserver();
  const { trigger: triggerIntersection } = useMockIntersectionObserver();

  const triggerChildMutation = () => {
    triggerMutation(document.body, { options: { childList: true, subtree: true } });
  };

  const triggerIntersectionWithRatio = img => {
    triggerIntersection(img, { entry: { intersectionRatio: 0.1 } });
  };

  const createLazyLoadImage = () => {
    const newImg = document.createElement('img');
    newImg.className = 'lazy';
    newImg.setAttribute('data-src', TEST_PATH);

    document.body.appendChild(newImg);
    triggerChildMutation();

    return newImg;
  };

  const createImage = () => {
    const newImg = document.createElement('img');
    newImg.setAttribute('src', TEST_PATH);

    document.body.appendChild(newImg);
    triggerChildMutation();

    return newImg;
  };

  const mockLoadEvent = () => {
    const addEventListener = window.addEventListener.bind(window);

    jest.spyOn(window, 'addEventListener').mockImplementation((event, callback) => {
      if (event === 'load') {
        callback();
      } else {
        addEventListener(event, callback);
      }
    });
  };

  beforeEach(() => {
    jest.spyOn(window, 'requestAnimationFrame').mockImplementation(execImmediately);
    jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
    jest.spyOn(LazyLoader, 'loadImage');

    mockLoadEvent();
  });

  afterEach(() => {
    document.body.innerHTML = '';
  });

  describe.each`
    hasIntersectionObserver | trigger
    ${true}                 | ${triggerIntersectionWithRatio}
    ${false}                | ${noop}
  `(
    'with hasIntersectionObserver=$hasIntersectionObserver',
    ({ hasIntersectionObserver, trigger }) => {
      let origIntersectionObserver;

      beforeEach(() => {
        origIntersectionObserver = global.IntersectionObserver;
        global.IntersectionObserver = hasIntersectionObserver
          ? global.IntersectionObserver
          : undefined;

        lazyLoader = new LazyLoader({
          observerNode: 'foobar',
        });
      });

      afterEach(() => {
        global.IntersectionObserver = origIntersectionObserver;
        lazyLoader.unregister();
      });

      it('determines intersection observer support', () => {
        expect(LazyLoader.supportsIntersectionObserver()).toBe(hasIntersectionObserver);
      });

      it('should copy value from data-src to src for img 1', () => {
        const img = createLazyLoadImage();

        // Doing everything that happens normally in onload
        lazyLoader.register();

        trigger(img);

        expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
        expect(img.getAttribute('src')).toBe(TEST_PATH);
        expect(img.getAttribute('data-src')).toBe(null);
        expect(img).toHaveClass('js-lazy-loaded');
      });

      it('should lazy load dynamically added data-src images', async () => {
        lazyLoader.register();

        const newImg = createLazyLoadImage();

        trigger(newImg);

        await waitForPromises();

        expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
        expect(newImg.getAttribute('src')).toBe(TEST_PATH);
        expect(newImg).toHaveClass('js-lazy-loaded');
      });

      it('should not alter normal images', () => {
        const newImg = createImage();

        lazyLoader.register();

        expect(LazyLoader.loadImage).not.toHaveBeenCalled();
        expect(newImg).not.toHaveClass('js-lazy-loaded');
      });

      it('should not load dynamically added pictures if content observer is turned off', async () => {
        lazyLoader.register();
        lazyLoader.stopContentObserver();

        const newImg = createLazyLoadImage();

        await waitForPromises();

        expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
        expect(newImg).not.toHaveClass('js-lazy-loaded');
      });

      it('should load dynamically added pictures if content observer is turned off and on again', async () => {
        lazyLoader.register();
        lazyLoader.stopContentObserver();
        lazyLoader.startContentObserver();

        const newImg = createLazyLoadImage();

        trigger(newImg);

        await waitForPromises();

        expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
        expect(newImg.getAttribute('src')).toBe(TEST_PATH);
        expect(newImg).toHaveClass('js-lazy-loaded');
      });
    },
  );
});
