/* eslint-disable global-require, promise/catch-or-return */

import path from 'path';

import axios from '~/lib/utils/axios_utils';

const absPath = path.join.bind(null, __dirname);

jest.mock('fs');
jest.mock('readdir-enhanced');

describe('mocks_helper.js', () => {
  let setupManualMocks;
  const setMock = jest.fn().mockName('setMock');
  let fs;
  let readdir;

  beforeAll(() => {
    jest.resetModules();
    jest.setMock = jest.fn().mockName('jest.setMock');
    fs = require('fs');
    readdir = require('readdir-enhanced');

    // We need to provide setupManualMocks with a mock function that pretends to do the setup of
    // the mock. This is because we can't mock jest.setMock across files.
    setupManualMocks = () => require('./mocks_helper').setupManualMocks(setMock);
  });

  afterEach(() => {
    fs.existsSync.mockReset();
    readdir.sync.mockReset();
    setMock.mockReset();
  });

  it('enumerates through mock file roots', () => {
    setupManualMocks();
    expect(fs.existsSync).toHaveBeenCalledTimes(2);
    expect(fs.existsSync).toHaveBeenNthCalledWith(1, absPath('ce'));
    expect(fs.existsSync).toHaveBeenNthCalledWith(2, absPath('node'));

    expect(readdir.sync).toHaveBeenCalledTimes(0);
  });

  it("doesn't traverse the directory tree infinitely", () => {
    fs.existsSync.mockReturnValue(true);
    readdir.sync.mockReturnValue([]);
    setupManualMocks();

    const readdirSpy = readdir.sync;
    expect(readdirSpy).toHaveBeenCalled();
    readdirSpy.mock.calls.forEach(call => {
      expect(call[1].deep).toBeLessThan(100);
    });
  });

  it('sets up mocks for CE (the ~/ prefix)', () => {
    fs.existsSync.mockImplementation(root => root.endsWith('ce'));
    readdir.sync.mockReturnValue(['root.js', 'lib/utils/util.js']);
    setupManualMocks();

    expect(readdir.sync).toHaveBeenCalledTimes(1);
    expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));

    expect(setMock).toHaveBeenCalledTimes(2);
    expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root');
    expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util');
  });

  it('sets up mocks for node_modules', () => {
    fs.existsSync.mockImplementation(root => root.endsWith('node'));
    readdir.sync.mockReturnValue(['jquery', '@babel/core']);
    setupManualMocks();

    expect(readdir.sync).toHaveBeenCalledTimes(1);
    expect(readdir.sync.mock.calls[0][0]).toBe(absPath('node'));

    expect(setMock).toHaveBeenCalledTimes(2);
    expect(setMock).toHaveBeenNthCalledWith(1, 'jquery', './node/jquery');
    expect(setMock).toHaveBeenNthCalledWith(2, '@babel/core', './node/@babel/core');
  });

  it('sets up mocks for all roots', () => {
    const files = {
      [absPath('ce')]: ['root', 'lib/utils/util'],
      [absPath('node')]: ['jquery', '@babel/core'],
    };

    fs.existsSync.mockReturnValue(true);
    readdir.sync.mockImplementation(root => files[root]);
    setupManualMocks();

    expect(readdir.sync).toHaveBeenCalledTimes(2);
    expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
    expect(readdir.sync.mock.calls[1][0]).toBe(absPath('node'));

    expect(setMock).toHaveBeenCalledTimes(4);
    expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root');
    expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util');
    expect(setMock).toHaveBeenNthCalledWith(3, 'jquery', './node/jquery');
    expect(setMock).toHaveBeenNthCalledWith(4, '@babel/core', './node/@babel/core');
  });

  it('fails when given a virtual mock', () => {
    fs.existsSync.mockImplementation(p => p.endsWith('ce'));
    readdir.sync.mockReturnValue(['virtual', 'shouldntBeImported']);
    setMock.mockImplementation(() => {
      throw new Error('Could not locate module');
    });

    expect(setupManualMocks).toThrow(
      new Error("A manual mock was defined for module ~/virtual, but the module doesn't exist!"),
    );

    expect(readdir.sync).toHaveBeenCalledTimes(1);
    expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
  });

  describe('auto-injection', () => {
    it('handles ambiguous paths', () => {
      jest.isolateModules(() => {
        const axios2 = require('../../../app/assets/javascripts/lib/utils/axios_utils').default;
        expect(axios2.isMock).toBe(true);
      });
    });

    it('survives jest.isolateModules()', done => {
      jest.isolateModules(() => {
        const axios2 = require('~/lib/utils/axios_utils').default;
        expect(axios2.get('http://gitlab.com'))
          .rejects.toThrow('Unexpected unmocked request')
          .then(done);
      });
    });

    it('can be unmocked and remocked', () => {
      jest.dontMock('~/lib/utils/axios_utils');
      jest.resetModules();
      const axios2 = require('~/lib/utils/axios_utils').default;
      expect(axios2).not.toBe(axios);
      expect(axios2.isMock).toBeUndefined();

      jest.doMock('~/lib/utils/axios_utils');
      jest.resetModules();
      const axios3 = require('~/lib/utils/axios_utils').default;
      expect(axios3).not.toBe(axios2);
      expect(axios3.isMock).toBe(true);
    });
  });
});
