'use strict';

// Flags: --experimental-vm-modules

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

const assert = require('assert');

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

async function expectsRejection(fn, settings) {
  const validateError = common.expectsError(settings);
  // Retain async context.
  const storedError = new Error('Thrown from:');
  try {
    await fn();
  } catch (err) {
    try {
      validateError(err);
    } catch (validationError) {
      console.error(validationError);
      console.error('Original error:');
      console.error(err);
      throw storedError;
    }
    return;
  }
  assert.fail('Missing expected exception');
}

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', () => {}, { url: 0 }, Symbol.iterator
  ]) {
    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 expectsRejection(async () => {
      const m = new SourceTextModule('');
      await m.link(invalidLinker);
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      type: TypeError
    });
  }
}

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

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

  common.expectsError(() => {
    const m = new SourceTextModule('');
    m.instantiate();
  }, {
    code: 'ERR_VM_MODULE_NOT_LINKED'
  });

  await expectsRejection(async () => {
    const m = new SourceTextModule('import "foo";');
    try {
      await m.link(common.mustCall(() => ({})));
    } catch {
      assert.strictEqual(m.linkingStatus, 'errored');
      m.instantiate();
    }
    assert.fail('Unreachable');
  }, {
    code: 'ERR_VM_MODULE_NOT_LINKED'
  });

  {
    const m = new SourceTextModule('import "foo";');
    await m.link(common.mustCall(async (specifier, module) => {
      assert.strictEqual(module, m);
      assert.strictEqual(specifier, 'foo');
      assert.strictEqual(m.linkingStatus, 'linking');
      common.expectsError(() => {
        m.instantiate();
      }, {
        code: 'ERR_VM_MODULE_NOT_LINKED'
      });
      return new SourceTextModule('');
    }));
    m.instantiate();
    await m.evaluate();
  }

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

  await expectsRejection(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'
  });

  await expectsRejection(async () => {
    const m = await createEmptyLinkedModule();
    await m.evaluate();
  }, {
    code: 'ERR_VM_MODULE_STATUS',
    message: 'Module status must be one of instantiated, evaluated, and errored'
  });

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

  await expectsRejection(async () => {
    const m = await createEmptyLinkedModule();
    m.instantiate();
    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 uninstantiated or instantiating'
  });

  await expectsRejection(async () => {
    const m = await createEmptyLinkedModule();
    m.namespace;
  }, {
    code: 'ERR_VM_MODULE_STATUS',
    message: 'Module status must not be uninstantiated or instantiating'
  });
}

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

  await expectsRejection(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.linkingStatus, 'errored');
      throw err;
    }
    assert.fail('Unreachable');
  }, {
    code: 'ERR_VM_MODULE_DIFFERENT_CONTEXT'
  });

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

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

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

    // There is no code for this exception since it is thrown by the JavaScript
    // engine.
    assert.throws(() => {
      m.instantiate();
    }, SyntaxError);
  })();

  await (async () => {
    const m = new SourceTextModule('throw new Error();');
    await m.link(common.mustNotCall());
    m.instantiate();
    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');
  })();
}

const finished = common.mustCall();

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