const createCJSTestRunner = (fname, tests, lang) => code => new Promise(
  (resolve, reject) => {
    const worker = new Worker('/workers/test-runner.js');

    worker.addEventListener('error', (ev) => {
      worker.terminate();
      reject(new Error(ev.message));
    });

    worker.addEventListener('message', (ev) => {
      worker.terminate();
      resolve(ev.data);
    });

    worker.postMessage({ fname, code, tests, lang });
  },
);

const createDOMTestRunner = (fname, tests, lang) => code => new Promise(
  (resolve, reject) => {
    const solutionId = `challenges/${fname}/solution`;
    const testsId = `challenges/${fname}/tests`;
    const prevSolutionIframe = document.getElementById(solutionId);
    const prevTestsIframe = document.getElementById(testsId);

    if (prevSolutionIframe) {
      prevSolutionIframe.remove();
    }

    if (prevTestsIframe) {
      prevTestsIframe.remove();
    }

    const solutionIframe = Object.assign(document.createElement('iframe'), {
      id: solutionId,
      srcdoc: code,
      style: 'position: absolute; width:0; height:0; border:0;',
    });

    solutionIframe.addEventListener('load', () => {
      const testsIframe = Object.assign(document.createElement('iframe'), {
        id: testsId,
        style: 'position: absolute; width:0; height:0; border:0;',
      });

      document.body.appendChild(testsIframe);

      const onMessage = (ev) => {
        const { type, ...testResults } = ev.data;

        if (type !== `@laboratoria/challenges/${fname}/test-results`) {
          return;
        }

        window.removeEventListener('message', onMessage);
        resolve(testResults);
      };

      window.addEventListener('message', onMessage);

      testsIframe.appendChild(Object.assign(document.createElement('script'), {
        type: 'module',
        textContent: `
          import 'https://cdnjs.cloudflare.com/ajax/libs/mocha/10.2.0/mocha.min.js';
          import 'https://cdnjs.cloudflare.com/ajax/libs/chai/4.3.7/chai.min.js';

          mocha.unloadFiles();
          mocha.suite.suites = [];
          mocha.suite._bail = false;

          mocha.setup({
            ui: 'bdd',
            reporter: function UnReporter(runner) {
              Mocha.reporters.Base.call(this, runner);
            },
            cleanReferencesAfterRun: false,
          });

          const testToJSON = test => ({
            title: test.title,
            fullTitle: test.fullTitle(),
            async: test.async,
            duration: test.duration,
            pending: test.pending,
            speed: test.speed || null,
            state: test.state,
            sync: test.sync,
            timedOut: test.timedOut,
            err: (test.err || {}).message || null,
          });

          const suiteToJSON = suite => ({
            title: suite.title,
            fullTitle: suite.fullTitle(),
            delayed: suite.delayed,
            pending: suite.pending,
            root: suite.root,
            suites: suite.suites.map(suiteToJSON),
            tests: suite.tests.map(testToJSON),
          });

          const { contentDocument } = window.parent.document.getElementById('${solutionId}');

          const requires = {
            chai: () => chai,
            ['../solution/${fname.replace(/\.html$/, '')}']: () => contentDocument,
            lang: () => '${lang}',
          };

          const require = (name) => {
            if (typeof requires[name] === 'function') {
              return requires[name]();
            }
          };

          ${tests}

          const runResults = mocha.run();

          runResults.on('end', () => {
            self.postMessage({
              type: '@laboratoria/challenges/${fname}/test-results',
              failures: runResults.failures,
              stats: runResults.stats,
              total: runResults.total,
              suite: suiteToJSON(runResults.suite),
            });
          });
        `,
      }));
    });

    document.body.appendChild(solutionIframe);
  },
);

const createFormTestRunner = (challenge, lang) => {
  const { questions } = challenge.intl[lang];

  return (data) => {
    const start = new Date();
    const errors = questions.reduce(
      (memo, question, idx) => {
        const solution = question.solution || { required: true };
        const solutionIsNull = [null, undefined, ''].includes(solution.value);
        const valueIsNull = [null, undefined, ''].includes(data[idx]);

        if (solution.required && valueIsNull) {
          return { ...memo, [idx]: 'value-missing-validation-error' };
        }

        if (solutionIsNull) {
          return memo;
        }

        if (Array.isArray(solution.value)) {
          return (
            JSON.stringify(solution.value.sort()) === JSON.stringify(data[idx]?.sort())
              ? memo
              : { ...memo, [idx]: 'array-missmatch' }
          );
        }

        return (
          solution.value === data[idx]
            ? memo
            : { ...memo, [idx]: 'value-missmatch' }
        );
      },
      {},
    );

    const total = questions.length;
    const failures = Object.keys(errors).length;
    const end = new Date();

    return {
      failures,
      stats: {
        start,
        end,
        duration: end - start,
        failures,
        passes: total - failures,
        tests: total,
      },
      total,
      errors,
    };
  };
};

const createTestRunner = (challenge, lang, fname) => {
  if (challenge.env === 'form') {
    return createFormTestRunner(challenge, lang);
  }

  const testsFname = fname.split('.').slice(0, -1).join('.');
  const tests = challenge.files[`/test/${testsFname}.spec.js`];

  return (
    challenge.env === 'dom'
      ? createDOMTestRunner(fname, tests, lang)
      : createCJSTestRunner(fname, tests, lang)
  )
};

export default createTestRunner;
