'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var log = require('lib0/logging');
var diff = require('./diff-9d236524.cjs');
var object = require('./object-c0c9435b.cjs');
var string = require('./string-fddc5f8b.cjs');
var math = require('./math-96d5e8c4.cjs');
var random = require('./random.cjs');
var prng = require('./prng-37d48618.cjs');
var statistics = require('./statistics-65f6114b.cjs');
var array = require('./array-78849c95.cjs');
var environment = require('./environment-1c97264d.cjs');
var json = require('./json-092190a1.cjs');
var time = require('./time-d8438852.cjs');
var promise = require('./promise-cda7b9bb.cjs');
var performance = require('lib0/performance');
var equality = require('./equality.cjs');
require('./function-314580f7.cjs');
require('./binary-ac8e39e2.cjs');
require('lib0/webcrypto');
require('./buffer-3e750729.cjs');
require('./encoding-1a745c43.cjs');
require('./number-1fb57bba.cjs');
require('./decoding-76e75827.cjs');
require('./error-0c1f634f.cjs');
require('./set-5b47859e.cjs');
require('./map-24d263c0.cjs');
require('./conditions-f5c0c102.cjs');
require('./storage.cjs');
require('./metric.cjs');

function _interopNamespace(e) {
  if (e && e.__esModule) return e;
  var n = Object.create(null);
  if (e) {
    Object.keys(e).forEach(function (k) {
      if (k !== 'default') {
        var d = Object.getOwnPropertyDescriptor(e, k);
        Object.defineProperty(n, k, d.get ? d : {
          enumerable: true,
          get: function () { return e[k]; }
        });
      }
    });
  }
  n["default"] = e;
  return Object.freeze(n);
}

var log__namespace = /*#__PURE__*/_interopNamespace(log);
var performance__namespace = /*#__PURE__*/_interopNamespace(performance);

/**
 * Testing framework with support for generating tests.
 *
 * ```js
 * // test.js template for creating a test executable
 * import { runTests } from 'lib0/testing'
 * import * as log from 'lib0/logging'
 * import * as mod1 from './mod1.test.js'
 * import * as mod2 from './mod2.test.js'

 * import { isBrowser, isNode } from 'lib0/environment.js'
 *
 * if (isBrowser) {
 *   // optional: if this is ran in the browser, attach a virtual console to the dom
 *   log.createVConsole(document.body)
 * }
 *
 * runTests({
 *  mod1,
 *  mod2,
 * }).then(success => {
 *   if (isNode) {
 *     process.exit(success ? 0 : 1)
 *   }
 * })
 * ```
 *
 * ```js
 * // mod1.test.js
 * /**
 *  * runTests automatically tests all exported functions that start with "test".
 *  * The name of the function should be in camelCase and is used for the logging output.
 *  *
 *  * @ param {t.TestCase} tc
 *  *\/
 * export const testMyFirstTest = tc => {
 *   t.compare({ a: 4 }, { a: 4 }, 'objects are equal')
 * }
 * ```
 *
 * Now you can simply run `node test.js` to run your test or run test.js in the browser.
 *
 * @module testing
 */
const extensive = environment.hasConf('extensive');

/* c8 ignore next */
const envSeed = environment.hasParam('--seed') ? Number.parseInt(environment.getParam('--seed', '0')) : null;

class TestCase {
  /**
   * @param {string} moduleName
   * @param {string} testName
   */
  constructor (moduleName, testName) {
    /**
     * @type {string}
     */
    this.moduleName = moduleName;
    /**
     * @type {string}
     */
    this.testName = testName;
    /**
     * This type can store custom information related to the TestCase
     *
     * @type {Map<string,any>}
     */
    this.meta = new Map();
    this._seed = null;
    this._prng = null;
  }

  resetSeed () {
    this._seed = null;
    this._prng = null;
  }

  /**
   * @type {number}
   */
  /* c8 ignore next */
  get seed () {
    /* c8 ignore else */
    if (this._seed === null) {
      /* c8 ignore next */
      this._seed = envSeed === null ? random.uint32() : envSeed;
    }
    return this._seed
  }

  /**
   * A PRNG for this test case. Use only this PRNG for randomness to make the test case reproducible.
   *
   * @type {prng.PRNG}
   */
  get prng () {
    /* c8 ignore else */
    if (this._prng === null) {
      this._prng = prng.create(this.seed);
    }
    return this._prng
  }
}

const repetitionTime = Number(environment.getParam('--repetition-time', '50'));
/* c8 ignore next */
const testFilter = environment.hasParam('--filter') ? environment.getParam('--filter', '') : null;

/* c8 ignore next */
const testFilterRegExp = testFilter !== null ? new RegExp(testFilter) : /.*/;

const repeatTestRegex = /^(repeat|repeating)\s/;

/**
 * @param {string} moduleName
 * @param {string} name
 * @param {function(TestCase):void|Promise<any>} f
 * @param {number} i
 * @param {number} numberOfTests
 */
const run = async (moduleName, name, f, i, numberOfTests) => {
  const uncamelized = string.fromCamelCase(name.slice(4), ' ');
  const filtered = !testFilterRegExp.test(`[${i + 1}/${numberOfTests}] ${moduleName}: ${uncamelized}`);
  /* c8 ignore next 3 */
  if (filtered) {
    return true
  }
  const tc = new TestCase(moduleName, name);
  const repeat = repeatTestRegex.test(uncamelized);
  const groupArgs = [log__namespace.GREY, `[${i + 1}/${numberOfTests}] `, log__namespace.PURPLE, `${moduleName}: `, log__namespace.BLUE, uncamelized];
  /* c8 ignore next 5 */
  if (testFilter === null) {
    log__namespace.groupCollapsed(...groupArgs);
  } else {
    log__namespace.group(...groupArgs);
  }
  const times = [];
  const start = performance__namespace.now();
  let lastTime = start;
  /**
   * @type {any}
   */
  let err = null;
  performance__namespace.mark(`${name}-start`);
  do {
    try {
      const p = f(tc);
      if (promise.isPromise(p)) {
        await p;
      }
    } catch (_err) {
      err = _err;
    }
    const currTime = performance__namespace.now();
    times.push(currTime - lastTime);
    lastTime = currTime;
    if (repeat && err === null && (lastTime - start) < repetitionTime) {
      tc.resetSeed();
    } else {
      break
    }
  } while (err === null && (lastTime - start) < repetitionTime)
  performance__namespace.mark(`${name}-end`);
  /* c8 ignore next 3 */
  if (err !== null && err.constructor !== SkipError) {
    log__namespace.printError(err);
  }
  performance__namespace.measure(name, `${name}-start`, `${name}-end`);
  log__namespace.groupEnd();
  const duration = lastTime - start;
  let success = true;
  times.sort((a, b) => a - b);
  /* c8 ignore next 3 */
  const againMessage = environment.isBrowser
    ? `     - ${window.location.host + window.location.pathname}?filter=\\[${i + 1}/${tc._seed === null ? '' : `&seed=${tc._seed}`}`
    : `\nrepeat: npm run test -- --filter "\\[${i + 1}/" ${tc._seed === null ? '' : `--seed ${tc._seed}`}`;
  const timeInfo = (repeat && err === null)
    ? ` - ${times.length} repetitions in ${time.humanizeDuration(duration)} (best: ${time.humanizeDuration(times[0])}, worst: ${time.humanizeDuration(array.last(times))}, median: ${time.humanizeDuration(statistics.median(times))}, average: ${time.humanizeDuration(statistics.average(times))})`
    : ` in ${time.humanizeDuration(duration)}`;
  if (err !== null) {
    /* c8 ignore start */
    if (err.constructor === SkipError) {
      log__namespace.print(log__namespace.GREY, log__namespace.BOLD, 'Skipped: ', log__namespace.UNBOLD, uncamelized);
    } else {
      success = false;
      log__namespace.print(log__namespace.RED, log__namespace.BOLD, 'Failure: ', log__namespace.UNBOLD, log__namespace.UNCOLOR, uncamelized, log__namespace.GREY, timeInfo, againMessage);
    }
    /* c8 ignore stop */
  } else {
    log__namespace.print(log__namespace.GREEN, log__namespace.BOLD, 'Success: ', log__namespace.UNBOLD, log__namespace.UNCOLOR, uncamelized, log__namespace.GREY, timeInfo, againMessage);
  }
  return success
};

/**
 * Describe what you are currently testing. The message will be logged.
 *
 * ```js
 * export const testMyFirstTest = tc => {
 *   t.describe('crunching numbers', 'already crunched 4 numbers!') // the optional second argument can describe the state.
 * }
 * ```
 *
 * @param {string} description
 * @param {string} info
 */
const describe = (description, info = '') => log__namespace.print(log__namespace.BLUE, description, ' ', log__namespace.GREY, info);

/**
 * Describe the state of the current computation.
 * ```js
 * export const testMyFirstTest = tc => {
 *   t.info(already crunched 4 numbers!') // the optional second argument can describe the state.
 * }
 * ```
 *
 * @param {string} info
 */
const info = info => describe('', info);

const printDom = log__namespace.printDom;

const printCanvas = log__namespace.printCanvas;

/**
 * Group outputs in a collapsible category.
 *
 * ```js
 * export const testMyFirstTest = tc => {
 *   t.group('subtest 1', () => {
 *     t.describe('this message is part of a collapsible section')
 *   })
 *   await t.groupAsync('subtest async 2', async () => {
 *     await someaction()
 *     t.describe('this message is part of a collapsible section')
 *   })
 * }
 * ```
 *
 * @param {string} description
 * @param {function(...any):void} f
 */
const group = (description, f) => {
  log__namespace.group(log__namespace.BLUE, description);
  try {
    f();
  } finally {
    log__namespace.groupEnd();
  }
};

/**
 * Group outputs in a collapsible category.
 *
 * ```js
 * export const testMyFirstTest = async tc => {
 *   t.group('subtest 1', () => {
 *     t.describe('this message is part of a collapsible section')
 *   })
 *   await t.groupAsync('subtest async 2', async () => {
 *     await someaction()
 *     t.describe('this message is part of a collapsible section')
 *   })
 * }
 * ```
 *
 * @param {string} description
 * @param {function(...any):Promise<any>} f
 */
const groupAsync = async (description, f) => {
  log__namespace.group(log__namespace.BLUE, description);
  try {
    await f();
  } finally {
    log__namespace.groupEnd();
  }
};

/**
 * Measure the time that it takes to calculate something.
 *
 * ```js
 * export const testMyFirstTest = async tc => {
 *   t.measureTime('measurement', () => {
 *     heavyCalculation()
 *   })
 *   await t.groupAsync('async measurement', async () => {
 *     await heavyAsyncCalculation()
 *   })
 * }
 * ```
 *
 * @param {string} message
 * @param {function(...any):void} f
 * @return {number} Returns a promise that resolves the measured duration to apply f
 */
const measureTime = (message, f) => {
  let duration;
  const start = performance__namespace.now();
  try {
    f();
  } finally {
    duration = performance__namespace.now() - start;
    log__namespace.print(log__namespace.PURPLE, message, log__namespace.GREY, ` ${time.humanizeDuration(duration)}`);
  }
  return duration
};

/**
 * Measure the time that it takes to calculate something.
 *
 * ```js
 * export const testMyFirstTest = async tc => {
 *   t.measureTimeAsync('measurement', async () => {
 *     await heavyCalculation()
 *   })
 *   await t.groupAsync('async measurement', async () => {
 *     await heavyAsyncCalculation()
 *   })
 * }
 * ```
 *
 * @param {string} message
 * @param {function(...any):Promise<any>} f
 * @return {Promise<number>} Returns a promise that resolves the measured duration to apply f
 */
const measureTimeAsync = async (message, f) => {
  let duration;
  const start = performance__namespace.now();
  try {
    await f();
  } finally {
    duration = performance__namespace.now() - start;
    log__namespace.print(log__namespace.PURPLE, message, log__namespace.GREY, ` ${time.humanizeDuration(duration)}`);
  }
  return duration
};

/**
 * @template T
 * @param {Array<T>} as
 * @param {Array<T>} bs
 * @param {string} [m]
 * @return {boolean}
 */
const compareArrays = (as, bs, m = 'Arrays match') => {
  if (as.length !== bs.length) {
    fail(m);
  }
  for (let i = 0; i < as.length; i++) {
    if (as[i] !== bs[i]) {
      fail(m);
    }
  }
  return true
};

/**
 * @param {string} a
 * @param {string} b
 * @param {string} [m]
 * @throws {TestError} Throws if tests fails
 */
const compareStrings = (a, b, m = 'Strings match') => {
  if (a !== b) {
    const diff$1 = diff.simpleDiffString(a, b);
    log__namespace.print(log__namespace.GREY, a.slice(0, diff$1.index), log__namespace.RED, a.slice(diff$1.index, diff$1.remove), log__namespace.GREEN, diff$1.insert, log__namespace.GREY, a.slice(diff$1.index + diff$1.remove));
    fail(m);
  }
};

/**
 * @template K,V
 * @param {Object<K,V>} a
 * @param {Object<K,V>} b
 * @param {string} [m]
 * @throws {TestError} Throws if test fails
 */
const compareObjects = (a, b, m = 'Objects match') => { object.equalFlat(a, b) || fail(m); };

/**
 * @param {any} _constructor
 * @param {any} a
 * @param {any} b
 * @param {string} path
 * @throws {TestError}
 */
const compareValues = (_constructor, a, b, path) => {
  if (a !== b) {
    fail(`Values ${json.stringify(a)} and ${json.stringify(b)} don't match (${path})`);
  }
  return true
};

/**
 * @param {string?} message
 * @param {string} reason
 * @param {string} path
 * @throws {TestError}
 */
const _failMessage = (message, reason, path) => fail(
  message === null
    ? `${reason} ${path}`
    : `${message} (${reason}) ${path}`
);

/**
 * @param {any} a
 * @param {any} b
 * @param {string} path
 * @param {string?} message
 * @param {function(any,any,any,string,any):boolean} customCompare
 */
const _compare = (a, b, path, message, customCompare) => {
  // we don't use assert here because we want to test all branches (istanbul errors if one branch is not tested)
  if (a == null || b == null) {
    return compareValues(null, a, b, path)
  }
  if (a[equality.EqualityTraitSymbol] != null) {
    if (a[equality.EqualityTraitSymbol](b)) {
      return true
    } else {
      _failMessage(message, 'Not equal by equality trait', path);
    }
  }
  if (a.constructor !== b.constructor) {
    _failMessage(message, 'Constructors don\'t match', path);
  }
  let success = true;
  switch (a.constructor) {
    case ArrayBuffer:
      a = new Uint8Array(a);
      b = new Uint8Array(b);
    // eslint-disable-next-line no-fallthrough
    case Uint8Array: {
      if (a.byteLength !== b.byteLength) {
        _failMessage(message, 'ArrayBuffer lengths match', path);
      }
      for (let i = 0; success && i < a.length; i++) {
        success = success && a[i] === b[i];
      }
      break
    }
    case Set: {
      if (a.size !== b.size) {
        _failMessage(message, 'Sets have different number of attributes', path);
      }
      // @ts-ignore
      a.forEach(value => {
        if (!b.has(value)) {
          _failMessage(message, `b.${path} does have ${value}`, path);
        }
      });
      break
    }
    case Map: {
      if (a.size !== b.size) {
        _failMessage(message, 'Maps have different number of attributes', path);
      }
      // @ts-ignore
      a.forEach((value, key) => {
        if (!b.has(key)) {
          _failMessage(message, `Property ${path}["${key}"] does not exist on second argument`, path);
        }
        _compare(value, b.get(key), `${path}["${key}"]`, message, customCompare);
      });
      break
    }
    case undefined: // undefined is often set as a constructor for objects
    case Object:
      if (object.length(a) !== object.length(b)) {
        _failMessage(message, 'Objects have a different number of attributes', path);
      }
      object.forEach(a, (value, key) => {
        if (!object.hasProperty(b, key)) {
          _failMessage(message, `Property ${path} does not exist on second argument`, path);
        }
        _compare(value, b[key], `${path}["${key}"]`, message, customCompare);
      });
      break
    case Array:
      if (a.length !== b.length) {
        _failMessage(message, 'Arrays have a different number of attributes', path);
      }
      // @ts-ignore
      a.forEach((value, i) => _compare(value, b[i], `${path}[${i}]`, message, customCompare));
      break
    /* c8 ignore next 4 */
    default:
      if (!customCompare(a.constructor, a, b, path, compareValues)) {
        _failMessage(message, `Values ${json.stringify(a)} and ${json.stringify(b)} don't match`, path);
      }
  }
  assert(success, message);
  return true
};

/**
 * @template T
 * @param {T} a
 * @param {T} b
 * @param {string?} [message]
 * @param {function(any,T,T,string,any):boolean} [customCompare]
 */
const compare = (a, b, message = null, customCompare = compareValues) => _compare(a, b, 'obj', message, customCompare);

/**
 * @template T
 * @param {T} property
 * @param {string?} [message]
 * @return {asserts property is NonNullable<T>}
 * @throws {TestError}
 */
/* c8 ignore next */
const assert = (property, message = null) => { property || fail(`Assertion failed${message !== null ? `: ${message}` : ''}`); };

/**
 * @param {function(...any):Promise<any>} f
 */
const promiseRejected = async f => {
  try {
    await f();
  } catch (err) {
    return
  }
  fail('Expected promise to fail');
};

/**
 * @param {function(...any):void} f
 * @throws {TestError}
 */
const fails = f => {
  try {
    f();
  } catch (_err) {
    log__namespace.print(log__namespace.GREEN, '⇖ This Error was expected');
    return
  }
  fail('Expected this to fail');
};

/**
 * @param {function(...any):Promise<any>} f
 * @throws {TestError}
 */
const failsAsync = async f => {
  try {
    await f();
  } catch (_err) {
    log__namespace.print(log__namespace.GREEN, '⇖ This Error was expected');
    return
  }
  fail('Expected this to fail');
};

/**
 * @param {Object<string, Object<string, function(TestCase):void|Promise<any>>>} tests
 */
const runTests = async tests => {
  /**
   * @param {string} testname
   */
  const filterTest = testname => testname.startsWith('test') || testname.startsWith('benchmark');
  const numberOfTests = object.map(tests, mod => object.map(mod, (f, fname) => /* c8 ignore next */ f && filterTest(fname) ? 1 : 0).reduce(math.add, 0)).reduce(math.add, 0);
  let successfulTests = 0;
  let testnumber = 0;
  const start = performance__namespace.now();
  for (const modName in tests) {
    const mod = tests[modName];
    for (const fname in mod) {
      const f = mod[fname];
      /* c8 ignore else */
      if (f && filterTest(fname)) {
        const repeatEachTest = 1;
        let success = true;
        for (let i = 0; success && i < repeatEachTest; i++) {
          success = await run(modName, fname, f, testnumber, numberOfTests);
        }
        testnumber++;
        /* c8 ignore else */
        if (success) {
          successfulTests++;
        }
      }
    }
  }
  const end = performance__namespace.now();
  log__namespace.print('');
  const success = successfulTests === numberOfTests;
  /* c8 ignore start */
  if (success) {
    log__namespace.print(log__namespace.GREEN, log__namespace.BOLD, 'All tests successful!', log__namespace.GREY, log__namespace.UNBOLD, ` in ${time.humanizeDuration(end - start)}`);
    log__namespace.printImgBase64(nyanCatImage, 50);
  } else {
    const failedTests = numberOfTests - successfulTests;
    log__namespace.print(log__namespace.RED, log__namespace.BOLD, `> ${failedTests} test${failedTests > 1 ? 's' : ''} failed`);
  }
  /* c8 ignore stop */
  return success
};

class TestError extends Error {}

/**
 * @param {string} reason
 * @throws {TestError}
 */
const fail = reason => {
  log__namespace.print(log__namespace.RED, log__namespace.BOLD, 'X ', log__namespace.UNBOLD, reason);
  throw new TestError('Test Failed')
};

class SkipError extends Error {}

/**
 * @param {boolean} cond If true, this tests will be skipped
 * @throws {SkipError}
 */
const skip = (cond = true) => {
  if (cond) {
    throw new SkipError('skipping..')
  }
};

// eslint-disable-next-line
const nyanCatImage = ' ';

exports.production = environment.production;
exports.TestCase = TestCase;
exports.assert = assert;
exports.compare = compare;
exports.compareArrays = compareArrays;
exports.compareObjects = compareObjects;
exports.compareStrings = compareStrings;
exports.describe = describe;
exports.envSeed = envSeed;
exports.extensive = extensive;
exports.fail = fail;
exports.fails = fails;
exports.failsAsync = failsAsync;
exports.group = group;
exports.groupAsync = groupAsync;
exports.info = info;
exports.measureTime = measureTime;
exports.measureTimeAsync = measureTimeAsync;
exports.printCanvas = printCanvas;
exports.printDom = printDom;
exports.promiseRejected = promiseRejected;
exports.repetitionTime = repetitionTime;
exports.run = run;
exports.runTests = runTests;
exports.skip = skip;
//# sourceMappingURL=testing.cjs.map
