import { launchDrawioEditor } from '~/drawio/drawio_editor';
import {
  DRAWIO_EDITOR_URL,
  DRAWIO_FRAME_ID,
  DIAGRAM_BACKGROUND_COLOR,
  DRAWIO_IFRAME_TIMEOUT,
  DIAGRAM_MAX_SIZE,
} from '~/drawio/constants';
import { createAlert, VARIANT_SUCCESS } from '~/alert';

jest.mock('~/alert');

jest.useFakeTimers();

describe('drawio/drawio_editor', () => {
  let editorFacade;
  let drawioIFrameReceivedMessages;
  const diagramURL = `${window.location.origin}/uploads/diagram.drawio.svg`;
  const testSvg = '<svg></svg>';
  const testEncodedSvg = `data:image/svg+xml;base64,${btoa(testSvg)}`;
  const filename = 'diagram.drawio.svg';

  const findDrawioIframe = () => document.getElementById(DRAWIO_FRAME_ID);
  const waitForDrawioIFrameMessage = ({ messageNumber = 1 } = {}) =>
    new Promise((resolve) => {
      let messageCounter = 0;
      const iframe = findDrawioIframe();

      iframe?.contentWindow.addEventListener('message', (event) => {
        drawioIFrameReceivedMessages.push(event);

        messageCounter += 1;

        if (messageCounter === messageNumber) {
          resolve();
        }
      });
    });
  const expectDrawioIframeMessage = ({ expectation, messageNumber = 1 }) => {
    expect(drawioIFrameReceivedMessages).toHaveLength(messageNumber);
    expect(JSON.parse(drawioIFrameReceivedMessages[messageNumber - 1].data)).toEqual(expectation);
  };
  const postMessageToParentWindow = (data) => {
    const event = new Event('message');

    Object.setPrototypeOf(event, {
      source: findDrawioIframe().contentWindow,
      data: JSON.stringify(data),
    });

    window.dispatchEvent(event);
  };

  beforeEach(() => {
    editorFacade = {
      getDiagram: jest.fn(),
      uploadDiagram: jest.fn(),
      insertDiagram: jest.fn(),
      updateDiagram: jest.fn(),
    };
    drawioIFrameReceivedMessages = [];
  });

  afterEach(() => {
    jest.clearAllMocks();
    findDrawioIframe()?.remove();
  });

  describe('initializing', () => {
    beforeEach(() => {
      launchDrawioEditor({ editorFacade });
    });

    it('creates the drawio editor iframe and attaches it to the body', () => {
      expect(findDrawioIframe().getAttribute('src')).toBe(DRAWIO_EDITOR_URL);
    });

    it('sets drawio-editor classname to the iframe', () => {
      expect(findDrawioIframe().classList).toContain('drawio-editor');
    });
  });

  describe(`when parent window does not receive configure event after ${DRAWIO_IFRAME_TIMEOUT} ms`, () => {
    beforeEach(() => {
      launchDrawioEditor({ editorFacade });
    });

    it('disposes draw.io iframe', () => {
      expect(findDrawioIframe()).not.toBe(null);
      jest.runAllTimers();
      expect(findDrawioIframe()).toBe(null);
    });

    it('displays an alert indicating that the draw.io editor could not be loaded', () => {
      jest.runAllTimers();

      expect(createAlert).toHaveBeenCalledWith({
        message: 'The diagrams.net editor could not be loaded.',
      });
    });
  });

  describe('when parent window receives configure event', () => {
    beforeEach(async () => {
      launchDrawioEditor({ editorFacade });
      postMessageToParentWindow({ event: 'configure' });

      await waitForDrawioIFrameMessage();
    });

    it('sends configure action to the draw.io iframe', async () => {
      expectDrawioIframeMessage({
        expectation: {
          action: 'configure',
          config: {
            darkColor: '#202020',
            settingsName: 'gitlab',
          },
          colorSchemeMeta: false,
        },
      });
    });

    it('does not remove the iframe after the load error timeouts run', async () => {
      jest.runAllTimers();

      expect(findDrawioIframe()).not.toBe(null);
    });
  });

  describe('when parent window receives init event', () => {
    describe('when there isn’t a diagram selected', () => {
      beforeEach(() => {
        editorFacade.getDiagram.mockResolvedValueOnce(null);

        launchDrawioEditor({ editorFacade });

        postMessageToParentWindow({ event: 'init' });
      });

      it('sends load action to the draw.io iframe with empty svg and title', async () => {
        await waitForDrawioIFrameMessage();

        expectDrawioIframeMessage({
          expectation: {
            action: 'load',
            xml: null,
            border: 8,
            background: DIAGRAM_BACKGROUND_COLOR,
            dark: false,
            title: null,
          },
        });
      });
    });

    describe('when there is a diagram selected', () => {
      const diagramSvg = '<svg></svg>';

      beforeEach(() => {
        editorFacade.getDiagram.mockResolvedValueOnce({
          diagramURL,
          diagramSvg,
          filename,
          contentType: 'image/svg+xml',
        });

        launchDrawioEditor({ editorFacade });
        postMessageToParentWindow({ event: 'init' });
      });

      it('sends load action to the draw.io iframe with the selected diagram svg and filename', async () => {
        await waitForDrawioIFrameMessage();

        // Step 5: The draw.io editor will send the downloaded diagram to the iframe
        expectDrawioIframeMessage({
          expectation: {
            action: 'load',
            xml: diagramSvg,
            border: 8,
            background: DIAGRAM_BACKGROUND_COLOR,
            dark: false,
            title: filename,
          },
        });
      });

      it('sets the drawio iframe as visible and resets cursor', async () => {
        await waitForDrawioIFrameMessage();

        expect(findDrawioIframe().style.visibility).toBe('visible');
        expect(findDrawioIframe().style.cursor).toBe('');
      });

      it('scrolls window to the top', async () => {
        await waitForDrawioIFrameMessage();

        expect(window.scrollX).toBe(0);
      });
    });

    describe.each`
      description | errorMessage | diagram
      ${'when there is an image selected that is not an svg file'} | ${'The selected image is not a valid SVG diagram'} | ${{
  diagramURL,
  contentType: 'image/png',
  filename: 'image.png',
}}
      ${'when the selected image is not an asset upload'} | ${'The selected image is not an asset uploaded in the application'} | ${{
  diagramSvg: '<svg></svg>',
  filename,
  contentType: 'image/svg+xml',
  diagramURL: 'https://example.com/image.drawio.svg',
}}
      ${'when the selected image is too large'} | ${'The selected image is too large.'} | ${{
  diagramSvg: 'x'.repeat(DIAGRAM_MAX_SIZE + 1),
  filename,
  contentType: 'image/svg+xml',
  diagramURL,
}}
    `('$description', ({ errorMessage, diagram }) => {
      beforeEach(() => {
        editorFacade.getDiagram.mockResolvedValueOnce(diagram);

        launchDrawioEditor({ editorFacade });

        postMessageToParentWindow({ event: 'init' });
      });

      it('displays an error alert indicating that the image is not a diagram', async () => {
        expect(createAlert).toHaveBeenCalledWith({
          message: errorMessage,
          error: expect.any(Error),
        });
      });

      it('disposes the draw.io diagram iframe', () => {
        expect(findDrawioIframe()).toBe(null);
      });
    });

    describe('when loading a diagram fails', () => {
      beforeEach(() => {
        editorFacade.getDiagram.mockRejectedValueOnce(new Error());

        launchDrawioEditor({ editorFacade });

        postMessageToParentWindow({ event: 'init' });
      });

      it('displays an error alert indicating the failure', async () => {
        expect(createAlert).toHaveBeenCalledWith({
          message: 'Cannot load the diagram into the diagrams.net editor',
          error: expect.any(Error),
        });
      });

      it('disposes the draw.io diagram iframe', () => {
        expect(findDrawioIframe()).toBe(null);
      });
    });
  });

  describe('when parent window receives prompt event', () => {
    describe('when the filename is empty', () => {
      beforeEach(() => {
        launchDrawioEditor({ editorFacade });

        postMessageToParentWindow({ event: 'prompt', value: '' });
      });

      it('sends prompt action to the draw.io iframe requesting a filename', async () => {
        await waitForDrawioIFrameMessage({ messageNumber: 1 });

        expectDrawioIframeMessage({
          expectation: {
            action: 'prompt',
            titleKey: 'filename',
            okKey: 'save',
            defaultValue: 'diagram.drawio.svg',
          },
          messageNumber: 1,
        });
      });

      it('sends dialog action to the draw.io iframe indicating that the filename cannot be empty', async () => {
        await waitForDrawioIFrameMessage({ messageNumber: 2 });

        expectDrawioIframeMessage({
          expectation: {
            action: 'dialog',
            titleKey: 'error',
            messageKey: 'filenameShort',
            buttonKey: 'ok',
          },
          messageNumber: 2,
        });
      });
    });

    describe('when the event data is not empty', () => {
      beforeEach(async () => {
        launchDrawioEditor({ editorFacade });
        postMessageToParentWindow({ event: 'prompt', value: 'diagram.drawio.svg' });

        await waitForDrawioIFrameMessage();
      });

      it('starts the saving file process', () => {
        expectDrawioIframeMessage({
          expectation: {
            action: 'spinner',
            show: true,
            messageKey: 'saving',
          },
        });
      });
    });
  });

  describe('when parent receives export event', () => {
    beforeEach(() => {
      editorFacade.uploadDiagram.mockResolvedValueOnce({});
    });

    it('reloads diagram in the draw.io editor', async () => {
      launchDrawioEditor({ editorFacade });
      postMessageToParentWindow({ event: 'export', data: testEncodedSvg });

      await waitForDrawioIFrameMessage();

      expectDrawioIframeMessage({
        expectation: expect.objectContaining({
          action: 'load',
          xml: expect.stringContaining(testSvg),
        }),
      });
    });

    it('marks the diagram as modified in the draw.io editor', async () => {
      launchDrawioEditor({ editorFacade });
      postMessageToParentWindow({ event: 'export', data: testEncodedSvg });

      await waitForDrawioIFrameMessage({ messageNumber: 2 });

      expectDrawioIframeMessage({
        expectation: expect.objectContaining({
          action: 'status',
          modified: true,
        }),
        messageNumber: 2,
      });
    });

    describe('when the diagram filename is set', () => {
      const TEST_FILENAME = 'diagram.drawio.svg';

      beforeEach(() => {
        launchDrawioEditor({ editorFacade, filename: TEST_FILENAME });
      });

      it('displays loading spinner in the draw.io editor', async () => {
        postMessageToParentWindow({ event: 'export', data: testEncodedSvg });

        await waitForDrawioIFrameMessage({ messageNumber: 3 });

        expectDrawioIframeMessage({
          expectation: {
            action: 'spinner',
            show: true,
            messageKey: 'saving',
          },
          messageNumber: 3,
        });
      });

      it('uploads exported diagram', async () => {
        postMessageToParentWindow({ event: 'export', data: testEncodedSvg });

        await waitForDrawioIFrameMessage({ messageNumber: 3 });

        expect(editorFacade.uploadDiagram).toHaveBeenCalledWith({
          filename: TEST_FILENAME,
          diagramSvg: expect.stringContaining(testSvg),
        });
      });

      describe('when uploading the exported diagram succeeds', () => {
        it('displays an alert indicating that the diagram was uploaded successfully', async () => {
          postMessageToParentWindow({ event: 'export', data: testEncodedSvg });

          await waitForDrawioIFrameMessage({ messageNumber: 3 });

          expect(createAlert).toHaveBeenCalledWith({
            message: expect.any(String),
            variant: VARIANT_SUCCESS,
            fadeTransition: true,
          });
        });

        it('disposes iframe', () => {
          jest.runAllTimers();

          expect(findDrawioIframe()).toBe(null);
        });
      });

      describe('when uploading the exported diagram fails', () => {
        const uploadError = new Error();

        beforeEach(() => {
          editorFacade.uploadDiagram.mockReset();
          editorFacade.uploadDiagram.mockRejectedValue(uploadError);

          postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
        });

        it('hides loading indicator in the draw.io editor', async () => {
          await waitForDrawioIFrameMessage({ messageNumber: 4 });

          expectDrawioIframeMessage({
            expectation: {
              action: 'spinner',
              show: false,
            },
            messageNumber: 4,
          });
        });

        it('displays an error dialog in the draw.io editor', async () => {
          await waitForDrawioIFrameMessage({ messageNumber: 5 });

          expectDrawioIframeMessage({
            expectation: {
              action: 'dialog',
              titleKey: 'error',
              modified: true,
              buttonKey: 'close',
              messageKey: 'errorSavingFile',
            },
            messageNumber: 5,
          });
        });
      });
    });

    describe('when diagram filename is not set', () => {
      it('sends prompt action to the draw.io iframe', async () => {
        launchDrawioEditor({ editorFacade });
        postMessageToParentWindow({ event: 'export', data: testEncodedSvg });

        await waitForDrawioIFrameMessage({ messageNumber: 3 });

        expect(drawioIFrameReceivedMessages[2].data).toEqual(
          JSON.stringify({
            action: 'prompt',
            titleKey: 'filename',
            okKey: 'save',
            defaultValue: 'diagram.drawio.svg',
          }),
        );
      });
    });
  });

  describe('when parent window receives exit event', () => {
    beforeEach(() => {
      launchDrawioEditor({ editorFacade });
    });

    it('disposes the the draw.io iframe', () => {
      expect(findDrawioIframe()).not.toBe(null);

      postMessageToParentWindow({ event: 'exit' });

      expect(findDrawioIframe()).toBe(null);
    });
  });
});