import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
import DesignPresentation from '~/design_management/components/design_presentation.vue';

const mockOverlayData = {
  overlayDimensions: {
    width: 100,
    height: 100,
  },
  overlayPosition: {
    top: '0',
    left: '0',
  },
};

describe('Design management design presentation component', () => {
  const originalGon = window.gon;
  let wrapper;

  function createComponent(
    {
      image,
      imageName,
      discussions = [],
      isAnnotating = false,
      resolvedDiscussionsExpanded = false,
    } = {},
    data = {},
    stubs = {},
  ) {
    wrapper = shallowMount(DesignPresentation, {
      propsData: {
        image,
        imageName,
        discussions,
        isAnnotating,
        resolvedDiscussionsExpanded,
        isLoading: false,
      },
      stubs,
    });

    // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
    // eslint-disable-next-line no-restricted-syntax
    wrapper.setData(data);
    wrapper.element.scrollTo = jest.fn();
  }

  const findOverlayCommentButton = () => wrapper.find('[data-qa-selector="design_image_button"]');

  /**
   * Spy on $refs and mock given values
   * @param {Object} viewportDimensions {width, height}
   * @param {Object} childDimensions {width, height}
   * @param {Float} scrollTopPerc 0 < x < 1
   * @param {Float} scrollLeftPerc  0 < x < 1
   */
  function mockRefDimensions(
    ref,
    viewportDimensions,
    childDimensions,
    scrollTopPerc,
    scrollLeftPerc,
  ) {
    jest.spyOn(ref, 'scrollWidth', 'get').mockReturnValue(childDimensions.width);
    jest.spyOn(ref, 'scrollHeight', 'get').mockReturnValue(childDimensions.height);
    jest.spyOn(ref, 'offsetWidth', 'get').mockReturnValue(viewportDimensions.width);
    jest.spyOn(ref, 'offsetHeight', 'get').mockReturnValue(viewportDimensions.height);
    jest
      .spyOn(ref, 'scrollLeft', 'get')
      .mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc);
    jest
      .spyOn(ref, 'scrollTop', 'get')
      .mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc);
  }

  async function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) {
    const event = useTouchEvents
      ? {
          mousedown: 'touchstart',
          mousemove: 'touchmove',
          mouseup: 'touchend',
        }
      : {
          mousedown: 'mousedown',
          mousemove: 'mousemove',
          mouseup: 'mouseup',
        };

    const addCommentOverlay = findOverlayCommentButton();

    // triggering mouse events on this element best simulates
    // reality, as it is the lowest-level node that needs to
    // respond to mouse events
    addCommentOverlay.trigger(event.mousedown, {
      clientX: startCoords.clientX,
      clientY: startCoords.clientY,
    });
    await nextTick();
    addCommentOverlay.trigger(event.mousemove, {
      clientX: endCoords.clientX,
      clientY: endCoords.clientY,
    });

    await nextTick();
    if (mouseup) {
      addCommentOverlay.trigger(event.mouseup);
      await nextTick();
    }
  }

  beforeEach(() => {
    window.gon = { current_user_id: 1 };
  });

  afterEach(() => {
    wrapper.destroy();
    window.gon = originalGon;
  });

  it('renders image and overlay when image provided', async () => {
    createComponent(
      {
        image: 'test.jpg',
        imageName: 'test',
      },
      mockOverlayData,
    );

    await nextTick();
    expect(wrapper.element).toMatchSnapshot();
  });

  it('renders empty state when no image provided', async () => {
    createComponent();

    await nextTick();
    expect(wrapper.element).toMatchSnapshot();
  });

  it('openCommentForm event emits correct data', async () => {
    createComponent(
      {
        image: 'test.jpg',
        imageName: 'test',
      },
      mockOverlayData,
    );

    wrapper.vm.openCommentForm({ x: 1, y: 1 });

    await nextTick();
    expect(wrapper.emitted('openCommentForm')).toEqual([
      [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }],
    ]);
  });

  describe('currentCommentForm', () => {
    it('is null when isAnnotating is false', async () => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        mockOverlayData,
      );

      await nextTick();
      expect(wrapper.vm.currentCommentForm).toBeNull();
      expect(wrapper.element).toMatchSnapshot();
    });

    it('is null when isAnnotating is true but annotation position is falsey', async () => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
          isAnnotating: true,
        },
        mockOverlayData,
      );

      await nextTick();
      expect(wrapper.vm.currentCommentForm).toBeNull();
      expect(wrapper.element).toMatchSnapshot();
    });

    it('is equal to current annotation position when isAnnotating is true', async () => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
          isAnnotating: true,
        },
        {
          ...mockOverlayData,
          currentAnnotationPosition: {
            x: 1,
            y: 1,
            width: 100,
            height: 100,
          },
        },
      );

      await nextTick();
      expect(wrapper.vm.currentCommentForm).toEqual({
        x: 1,
        y: 1,
        width: 100,
        height: 100,
      });
      expect(wrapper.element).toMatchSnapshot();
    });
  });

  describe('setOverlayPosition', () => {
    beforeEach(() => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        mockOverlayData,
      );
    });

    afterEach(() => {
      jest.clearAllMocks();
    });

    it('sets overlay position correctly when overlay is smaller than viewport', () => {
      jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
      jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);

      wrapper.vm.setOverlayPosition();
      expect(wrapper.vm.overlayPosition).toEqual({
        left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
        top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
      });
    });

    it('sets overlay position correctly when overlay width is larger than viewports', () => {
      jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(50);
      jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);

      wrapper.vm.setOverlayPosition();
      expect(wrapper.vm.overlayPosition).toEqual({
        left: '0',
        top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
      });
    });

    it('sets overlay position correctly when overlay height is larger than viewports', () => {
      jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
      jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(50);

      wrapper.vm.setOverlayPosition();
      expect(wrapper.vm.overlayPosition).toEqual({
        left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
        top: '0',
      });
    });
  });

  describe('getViewportCenter', () => {
    beforeEach(() => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        mockOverlayData,
      );
    });

    it('calculate center correctly with no scroll', () => {
      mockRefDimensions(
        wrapper.vm.$refs.presentationViewport,
        { width: 10, height: 10 },
        { width: 20, height: 20 },
        0,
        0,
      );

      expect(wrapper.vm.getViewportCenter()).toEqual({
        x: 5,
        y: 5,
      });
    });

    it('calculate center correctly with some scroll', () => {
      mockRefDimensions(
        wrapper.vm.$refs.presentationViewport,
        { width: 10, height: 10 },
        { width: 20, height: 20 },
        0.5,
        0.5,
      );

      expect(wrapper.vm.getViewportCenter()).toEqual({
        x: 10,
        y: 10,
      });
    });

    it('Returns default case if no overflow (scrollWidth==offsetWidth, etc.)', () => {
      mockRefDimensions(
        wrapper.vm.$refs.presentationViewport,
        { width: 20, height: 20 },
        { width: 20, height: 20 },
        0.5,
        0.5,
      );

      expect(wrapper.vm.getViewportCenter()).toEqual({
        x: 10,
        y: 10,
      });
    });
  });

  describe('scaleZoomFocalPoint', () => {
    it('scales focal point correctly when zooming in', () => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        {
          ...mockOverlayData,
          zoomFocalPoint: {
            x: 5,
            y: 5,
            width: 50,
            height: 50,
          },
        },
      );

      wrapper.vm.scaleZoomFocalPoint();
      expect(wrapper.vm.zoomFocalPoint).toEqual({
        x: 10,
        y: 10,
        width: 100,
        height: 100,
      });
    });

    it('scales focal point correctly when zooming out', () => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        {
          ...mockOverlayData,
          zoomFocalPoint: {
            x: 10,
            y: 10,
            width: 200,
            height: 200,
          },
        },
      );

      wrapper.vm.scaleZoomFocalPoint();
      expect(wrapper.vm.zoomFocalPoint).toEqual({
        x: 5,
        y: 5,
        width: 100,
        height: 100,
      });
    });
  });

  describe('onImageResize', () => {
    beforeEach(async () => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        mockOverlayData,
      );

      jest.spyOn(wrapper.vm, 'shiftZoomFocalPoint');
      jest.spyOn(wrapper.vm, 'scaleZoomFocalPoint');
      jest.spyOn(wrapper.vm, 'scrollToFocalPoint');
      wrapper.vm.onImageResize({ width: 10, height: 10 });
      await nextTick();
    });

    it('sets zoom focal point on initial load', () => {
      expect(wrapper.vm.shiftZoomFocalPoint).toHaveBeenCalled();
      expect(wrapper.vm.initialLoad).toBe(false);
    });

    it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', async () => {
      wrapper.vm.onImageResize({ width: 10, height: 10 });
      await nextTick();
      expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled();
      expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled();
    });
  });

  describe('onPresentationMousedown', () => {
    it.each`
      scenario                        | width  | height
      ${'width overflows'}            | ${101} | ${100}
      ${'height overflows'}           | ${100} | ${101}
      ${'width and height overflows'} | ${200} | ${200}
    `('sets lastDragPosition when design $scenario', ({ width, height }) => {
      createComponent();
      mockRefDimensions(
        wrapper.vm.$refs.presentationViewport,
        { width: 100, height: 100 },
        { width, height },
      );

      const newLastDragPosition = { x: 2, y: 2 };
      wrapper.vm.onPresentationMousedown({
        clientX: newLastDragPosition.x,
        clientY: newLastDragPosition.y,
      });

      expect(wrapper.vm.lastDragPosition).toStrictEqual(newLastDragPosition);
    });

    it('does not set lastDragPosition if design does not overflow', () => {
      const lastDragPosition = { x: 1, y: 1 };

      createComponent({}, { lastDragPosition });
      mockRefDimensions(
        wrapper.vm.$refs.presentationViewport,
        { width: 100, height: 100 },
        { width: 50, height: 50 },
      );

      wrapper.vm.onPresentationMousedown({ clientX: 2, clientY: 2 });

      // check lastDragPosition is unchanged
      expect(wrapper.vm.lastDragPosition).toStrictEqual(lastDragPosition);
    });
  });

  describe('getAnnotationPositon', () => {
    it.each`
      coordinates               | overlayDimensions                | position
      ${{ x: 100, y: 100 }}     | ${{ width: 50, height: 50 }}     | ${{ x: 100, y: 100, width: 50, height: 50 }}
      ${{ x: 100.2, y: 100.5 }} | ${{ width: 50.6, height: 50.0 }} | ${{ x: 100, y: 101, width: 51, height: 50 }}
    `('returns correct annotation position', ({ coordinates, overlayDimensions, position }) => {
      createComponent(undefined, {
        overlayDimensions: {
          width: overlayDimensions.width,
          height: overlayDimensions.height,
        },
      });

      expect(wrapper.vm.getAnnotationPositon(coordinates)).toStrictEqual(position);
    });
  });

  describe('when design is overflowing', () => {
    beforeEach(() => {
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        mockOverlayData,
        {
          'design-overlay': DesignOverlay,
        },
      );

      // mock a design that overflows
      mockRefDimensions(
        wrapper.vm.$refs.presentationViewport,
        { width: 10, height: 10 },
        { width: 20, height: 20 },
        0,
        0,
      );
    });

    it('opens a comment form if design was not dragged', async () => {
      const addCommentOverlay = findOverlayCommentButton();
      const startCoords = {
        clientX: 1,
        clientY: 1,
      };

      addCommentOverlay.trigger('mousedown', {
        clientX: startCoords.clientX,
        clientY: startCoords.clientY,
      });

      await nextTick();
      addCommentOverlay.trigger('mouseup');
      await nextTick();
      expect(wrapper.emitted('openCommentForm')).toBeDefined();
    });

    describe('when clicking and dragging', () => {
      it.each`
        description               | useTouchEvents
        ${'with touch events'}    | ${true}
        ${'without touch events'} | ${false}
      `('calls scrollTo with correct arguments $description', ({ useTouchEvents }) => {
        return clickDragExplore(
          { clientX: 0, clientY: 0 },
          { clientX: 10, clientY: 10 },
          { useTouchEvents },
        ).then(() => {
          expect(wrapper.element.scrollTo).toHaveBeenCalledTimes(1);
          expect(wrapper.element.scrollTo).toHaveBeenCalledWith(-10, -10);
        });
      });

      it('does not open a comment form when drag position exceeds buffer', () => {
        return clickDragExplore(
          { clientX: 0, clientY: 0 },
          { clientX: 10, clientY: 10 },
          { mouseup: true },
        ).then(() => {
          expect(wrapper.emitted('openCommentForm')).toBeUndefined();
        });
      });

      it('opens a comment form when drag position is within buffer', () => {
        return clickDragExplore(
          { clientX: 0, clientY: 0 },
          { clientX: 1, clientY: 0 },
          { mouseup: true },
        ).then(() => {
          expect(wrapper.emitted('openCommentForm')).toBeDefined();
        });
      });
    });
  });

  describe('when user is not logged in', () => {
    beforeEach(() => {
      window.gon = { current_user_id: null };
      createComponent(
        {
          image: 'test.jpg',
          imageName: 'test',
        },
        mockOverlayData,
      );
    });

    it('disables commenting from design overlay', () => {
      expect(wrapper.findComponent(DesignOverlay).props()).toMatchObject({
        disableCommenting: true,
      });
    });
  });
});