'use strict';

// Flags: --experimental-vm-modules

const common = require('../common');

const assert = require('assert');

const { SourceTextModule, createContext } = require('vm');

async function createEmptyLinkedModule() {
  const m = new SourceTextModule('');
  await m.link(common.mustNotCall());
  return m;
}

async function checkArgType() {
  common.expectsError(() => {
    new SourceTextModule();
  }, {
    code: 'ERR_INVALID_ARG_TYPE',
    type: TypeError
  });

  for (const invalidOptions of [
    0, 1, null, true, 'str', () => {}, { identifier: 0 }, Symbol.iterator,
    { context: null }, { context: 'hucairz' }, { context: {} }
  ]) {
    common.expectsError(() => {
      new SourceTextModule('', invalidOptions);
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      type: TypeError
    });
  }

  for (const invalidLinker of [
    0, 1, undefined, null, true, 'str', {}, Symbol.iterator
  ]) {
    await assert.rejects(async () => {
      const m = new SourceTextModule('');
      await m.link(invalidLinker);
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError'
    });
  }
}

// Check methods/properties can only be used under a specific state.
async function checkModuleState() {
  await assert.rejects(async () => {
    const m = new SourceTextModule('');
    await m.link(common.mustNotCall());
    assert.strictEqual(m.status, 'linked');
    await m.link(common.mustNotCall());
  }, {
    code: 'ERR_VM_MODULE_ALREADY_LINKED'
  });

  await assert.rejects(async () => {
    const m = new SourceTextModule('');
    m.link(common.mustNotCall());
    assert.strictEqual(m.status, 'linking');
    await m.link(common.mustNotCall());
  }, {
    code: 'ERR_VM_MODULE_STATUS'
  });

  await assert.rejects(async () => {
    const m = new SourceTextModule('');
    await m.evaluate();
  }, {
    code: 'ERR_VM_MODULE_STATUS',
    message: 'Module status must be one of linked, evaluated, or errored'
  });

  await assert.rejects(async () => {
    const m = new SourceTextModule('');
    await m.evaluate(false);
  }, {
    code: 'ERR_INVALID_ARG_TYPE',
    message: 'The "options" argument must be of type Object. ' +
             'Received type boolean'
  });

  common.expectsError(() => {
    const m = new SourceTextModule('');
    m.error;
  }, {
    code: 'ERR_VM_MODULE_STATUS',
    message: 'Module status must be errored'
  });

  await assert.rejects(async () => {
    const m = await createEmptyLinkedModule();
    await m.evaluate();
    m.error;
  }, {
    code: 'ERR_VM_MODULE_STATUS',
    message: 'Module status must be errored'
  });

  common.expectsError(() => {
    const m = new SourceTextModule('');
    m.namespace;
  }, {
    code: 'ERR_VM_MODULE_STATUS',
    message: 'Module status must not be unlinked or linking'
  });
}

// Check link() fails when the returned module is not valid.
async function checkLinking() {
  await assert.rejects(async () => {
    const m = new SourceTextModule('import "foo";');
    try {
      await m.link(common.mustCall(() => ({})));
    } catch (err) {
      assert.strictEqual(m.status, 'errored');
      throw err;
    }
  }, {
    code: 'ERR_VM_MODULE_NOT_MODULE'
  });

  await assert.rejects(async () => {
    const c = createContext({ a: 1 });
    const foo = new SourceTextModule('', { context: c });
    await foo.link(common.mustNotCall());
    const bar = new SourceTextModule('import "foo";');
    try {
      await bar.link(common.mustCall(() => foo));
    } catch (err) {
      assert.strictEqual(bar.status, 'errored');
      throw err;
    }
  }, {
    code: 'ERR_VM_MODULE_DIFFERENT_CONTEXT'
  });

  await assert.rejects(async () => {
    const erroredModule = new SourceTextModule('import "foo";');
    try {
      await erroredModule.link(common.mustCall(() => ({})));
    } catch {
      // ignored
    } finally {
      assert.strictEqual(erroredModule.status, 'errored');
    }

    const rootModule = new SourceTextModule('import "errored";');
    await rootModule.link(common.mustCall(() => erroredModule));
  }, {
    code: 'ERR_VM_MODULE_LINKING_ERRORED'
  });
}

common.expectsError(() => {
  new SourceTextModule('', {
    importModuleDynamically: 'hucairz'
  });
}, {
  code: 'ERR_INVALID_ARG_TYPE',
  type: TypeError,
  message: 'The "options.importModuleDynamically"' +
    ' property must be of type function. Received type string'
});

// Check the JavaScript engine deals with exceptions correctly
async function checkExecution() {
  await (async () => {
    const m = new SourceTextModule('import { nonexistent } from "module";');

    // There is no code for this exception since it is thrown by the JavaScript
    // engine.
    await assert.rejects(() => {
      return m.link(common.mustCall(() => new SourceTextModule('')));
    }, SyntaxError);
  })();

  await (async () => {
    const m = new SourceTextModule('throw new Error();');
    await m.link(common.mustNotCall());
    const evaluatePromise = m.evaluate();
    await evaluatePromise.catch(() => {});
    assert.strictEqual(m.status, 'errored');
    try {
      await evaluatePromise;
    } catch (err) {
      assert.strictEqual(m.error, err);
      return;
    }
    assert.fail('Missing expected exception');
  })();
}

// Check for error thrown when breakOnSigint is not a boolean for evaluate()
async function checkInvalidOptionForEvaluate() {
  await assert.rejects(async () => {
    const m = new SourceTextModule('export const a = 1; export var b = 2');
    await m.evaluate({ breakOnSigint: 'a-string' });
  }, {
    name: 'TypeError',
    message:
      'The "options.breakOnSigint" property must be of type boolean. ' +
      'Received type string',
    code: 'ERR_INVALID_ARG_TYPE'
  });
}

const finished = common.mustCall();

(async function main() {
  await checkArgType();
  await checkModuleState();
  await checkLinking();
  await checkExecution();
  await checkInvalidOptionForEvaluate();
  finished();
})();
