import path from 'path';
import dedent from 'dedent';
import * as babel from '@babel/core';
import { Module } from '../src';
import type { Evaluator, StrictOptions } from '../src';

beforeEach(() => Module.invalidate());

const evaluator: Evaluator = (filename, options, text) => {
  const { code } = babel.transformSync(text, {
    filename: filename,
  })!;
  return [code!, null];
};

function getFileName() {
  return path.resolve(__dirname, `../__fixtures__/test.js`);
}

const options: StrictOptions = {
  displayName: false,
  evaluate: true,
  rules: [
    {
      action: evaluator,
    },
    {
      test: /\/node_modules\//,
      action: 'ignore',
    },
  ],
  babelOptions: {},
};

beforeEach(() => Module.invalidateEvalCache());

it('creates module for JS files', () => {
  const filename = '/foo/bar/test.js';
  const mod = new Module(filename, options);

  mod.evaluate('module.exports = () => 42');

  expect(mod.exports()).toBe(42);
  expect(mod.id).toBe(filename);
  expect(mod.filename).toBe(filename);
});

it('requires JS files', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
    const answer = require('./sample-script');

    module.exports = 'The answer is ' + answer;
  `);

  expect(mod.exports).toBe('The answer is 42');
});

it('requires JSON files', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
    const data = require('./sample-data.json');

    module.exports = 'Our saviour, ' + data.name;
  `);

  expect(mod.exports).toBe('Our saviour, Luke Skywalker');
});

it('imports JS files', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
    import answer from './sample-script';

    export const result = 'The answer is ' + answer;
  `);

  expect(mod.exports.result).toBe('The answer is 42');
});

it('imports TypeScript files', () => {
  const mod = new Module(
    path.resolve(__dirname, '../__fixtures__/test.ts'),
    options
  );

  mod.evaluate(dedent`
    import answer from './sample-typescript';

    export const result = 'The answer is ' + answer;
  `);

  expect(mod.exports.result).toBe('The answer is 27');
});

it('imports JSON files', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
    import data from './sample-data.json';

    const result = 'Our saviour, ' + data.name;

    export default result;
  `);

  expect(mod.exports.default).toBe('Our saviour, Luke Skywalker');
});

it('returns module from the cache', () => {
  /* eslint-disable no-self-compare */

  const filename = getFileName();
  const mod = new Module(filename, options);
  const id = './sample-data.json';

  expect(mod.require(id) === mod.require(id)).toBe(true);

  expect(
    new Module(filename, options).require(id) ===
      new Module(filename, options).require(id)
  ).toBe(true);
});

it('clears modules from the cache', () => {
  const filename = getFileName();
  const id = './sample-data.json';

  const result = new Module(filename, options).require(id);

  expect(result === new Module(filename, options).require(id)).toBe(true);

  Module.invalidate();

  expect(result === new Module(filename, options).require(id)).toBe(false);
});

it('exports the path for non JS/JSON files', () => {
  const mod = new Module(getFileName(), options);

  expect(mod.require('./sample-asset.png')).toBe('./sample-asset.png');
});

it('returns module when requiring mocked builtin node modules', () => {
  const mod = new Module(getFileName(), options);

  expect(mod.require('path')).toBe(require('path'));
});

it('returns null when requiring empty builtin node modules', () => {
  const mod = new Module(getFileName(), options);

  expect(mod.require('fs')).toBe(null);
});

it('throws when requiring unmocked builtin node modules', () => {
  const mod = new Module(getFileName(), options);

  expect(() => mod.require('perf_hooks')).toThrow(
    'Unable to import "perf_hooks". Importing Node builtins is not supported in the sandbox.'
  );
});

it('has access to the global object', () => {
  const mod = new Module(getFileName(), options);

  expect(() =>
    mod.evaluate(dedent`
    new global.Set();
  `)
  ).not.toThrow();
});

it("doesn't have access to the process object", () => {
  const mod = new Module(getFileName(), options);

  expect(() =>
    mod.evaluate(dedent`
    module.exports = process.abort();
  `)
  ).toThrow('process.abort is not a function');
});

it('has access to NODE_ENV', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
  module.exports = process.env.NODE_ENV;
  `);

  expect(mod.exports).toBe(process.env.NODE_ENV);
});

it('has require.resolve available', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
  module.exports = require.resolve('./sample-script');
  `);

  expect(mod.exports).toBe(
    path.resolve(path.dirname(mod.filename), 'sample-script.js')
  );
});

it('has require.ensure available', () => {
  const mod = new Module(getFileName(), options);

  expect(() =>
    mod.evaluate(dedent`
  require.ensure(['./sample-script']);
  `)
  ).not.toThrow();
});

it('has __filename available', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
  module.exports = __filename;
  `);

  expect(mod.exports).toBe(mod.filename);
});

it('has __dirname available', () => {
  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
  module.exports = __dirname;
  `);

  expect(mod.exports).toBe(path.dirname(mod.filename));
});

it('has setTimeout, clearTimeout available', () => {
  const mod = new Module(getFileName(), options);

  expect(() =>
    mod.evaluate(dedent`
  const x = setTimeout(() => {
    console.log('test');
  },0);

  clearTimeout(x);
  `)
  ).not.toThrow();
});

it('has setInterval, clearInterval available', () => {
  const mod = new Module(getFileName(), options);

  expect(() =>
    mod.evaluate(dedent`
  const x = setInterval(() => {
    console.log('test');
  }, 1000);

  clearInterval(x);
  `)
  ).not.toThrow();
});

it('has setImmediate, clearImmediate available', () => {
  const mod = new Module(getFileName(), options);

  expect(() =>
    mod.evaluate(dedent`
  const x = setImmediate(() => {
    console.log('test');
  });

  clearImmediate(x);
  `)
  ).not.toThrow();
});

it('has global objects available without referencing global', () => {
  const mod = new Module(getFileName(), options);

  expect(() =>
    mod.evaluate(dedent`
  const x = new Set();
  `)
  ).not.toThrow();
});

it('changes resolve behaviour on overriding _resolveFilename', () => {
  const originalResolveFilename = Module._resolveFilename;

  Module._resolveFilename = (id) => (id === 'foo' ? 'bar' : id);

  const mod = new Module(getFileName(), options);

  mod.evaluate(dedent`
  module.exports = [
    require.resolve('foo'),
    require.resolve('test'),
  ];
  `);

  // Restore old behavior
  Module._resolveFilename = originalResolveFilename;

  expect(mod.exports).toEqual(['bar', 'test']);
});

it('correctly processes export declarations in strict mode', () => {
  const filename = '/foo/bar/test.js';
  const mod = new Module(filename, options);

  mod.evaluate('"use strict"; exports = module.exports = () => 42');

  expect(mod.exports()).toBe(42);
  expect(mod.id).toBe(filename);
  expect(mod.filename).toBe(filename);
});
