/* eslint-disable @typescript-eslint/no-var-requires */

const { parseGrep, shouldTestRun } = require("./utils");
const version = 10;
const debug = require("debug")("cypress-grep");
debug.log = console.info.bind(console);

// preserve the real "it" function
const _it = it;
const _describe = describe;

/**
 * Wraps the "it" and "describe" functions that support tags.
 * @see https://github.com/cypress-io/cypress-grep
 */
function cypressGrep() {
  /** @type {string} Part of the test title go grep */
  let grep = Cypress.env("grep");
  if (grep) {
    grep = String(grep).trim();
  }

  /** @type {string} Raw tags to grep string */
  const grepTags = Cypress.env("grepTags") || Cypress.env("grep-tags");

  const burnSpecified =
    Cypress.env("grepBurn") || Cypress.env("grep-burn") || Cypress.env("burn");

  const grepUntagged =
    Cypress.env("grepUntagged") || Cypress.env("grep-untagged");

  if (!grep && !grepTags && !burnSpecified && !grepUntagged) {
    // nothing to do, the user has no specified the "grep" string
    debug("Nothing to grep, version %s", version);
    return;
  }

  /** @type {number} Number of times to repeat each running test */
  const grepBurn =
    Cypress.env("grepBurn") ||
    Cypress.env("grep-burn") ||
    Cypress.env("burn") ||
    1;

  /** @type {boolean} Omit filtered tests completely */
  const omitFiltered =
    Cypress.env("grepOmitFiltered") || Cypress.env("grep-omit-filtered");

  debug("grep %o", { grep, grepTags, grepBurn, omitFiltered, version });
  if (!Cypress._.isInteger(grepBurn) || grepBurn < 1) {
    throw new Error(`Invalid grep burn value: ${grepBurn}`);
  }

  const parsedGrep = parseGrep(grep, grepTags);
  debug("parsed grep %o", parsedGrep);

  // prevent multiple registrations
  // https://github.com/cypress-io/cypress-grep/issues/59
  if (it.name === "itGrep") {
    debug("already registered cypress-grep");
    return;
  }

  it = function itGrep(name, options, callback) {
    if (typeof options === "function") {
      // the test has format it('...', cb)
      callback = options;
      options = {};
    }

    if (!callback) {
      // the pending test by itself
      return _it(name, options);
    }

    let configTags = options && options.tags;
    if (typeof configTags === "string") {
      configTags = [configTags];
    }

    const nameToGrep = suiteStack
      .map(item => item.name)
      .concat(name)
      .join(" ");
    const tagsToGrep = suiteStack
      .flatMap(item => item.tags)
      .concat(configTags)
      .filter(Boolean);

    const shouldRun = shouldTestRun(
      parsedGrep,
      nameToGrep,
      tagsToGrep,
      grepUntagged,
    );

    if (tagsToGrep && tagsToGrep.length) {
      debug(
        'should test "%s" with tags %s run? %s',
        name,
        tagsToGrep.join(","),
        shouldRun,
      );
    } else {
      debug('should test "%s" run? %s', nameToGrep, shouldRun);
    }

    if (shouldRun) {
      if (grepBurn > 1) {
        // repeat the same test to make sure it is solid
        return Cypress._.times(grepBurn, k => {
          const fullName = `${name}: burning ${k + 1} of ${grepBurn}`;
          _it(fullName, options, callback);
        });
      }
      return _it(name, options, callback);
    }

    if (omitFiltered) {
      // omit the filtered tests completely
      return;
    } else {
      // skip tests without grep string in their names
      return _it.skip(name, options, callback);
    }
  };

  // list of "describe" suites for the current test
  // when we encounter a new suite, we push it to the stack
  // when the "describe" function exits, we pop it
  // Thus a test can look up the tags from its parent suites
  const suiteStack = [];

  describe = function describeGrep(name, options, callback) {
    if (typeof options === "function") {
      // the block has format describe('...', cb)
      callback = options;
      options = {};
    }

    const stackItem = { name };
    suiteStack.push(stackItem);

    if (!callback) {
      // the pending suite by itself
      const result = _describe(name, options);
      suiteStack.pop();
      return result;
    }

    let configTags = options && options.tags;
    if (typeof configTags === "string") {
      configTags = [configTags];
    }

    if (!configTags || !configTags.length) {
      // if the describe suite does not have explicit tags
      // move on, since the tests inside can have their own tags
      _describe(name, options, callback);
      suiteStack.pop();
      return;
    }

    // when looking at the suite of the tests, I found
    // that using the name is quickly becoming very confusing
    // and thus we need to use the explicit tags
    stackItem.tags = configTags;
    _describe(name, options, callback);
    suiteStack.pop();

    return;
  };

  // overwrite "context" which is an alias to "describe"
  context = describe;

  // overwrite "specify" which is an alias to "it"
  specify = it;

  // keep the ".skip", ".only" methods the same as before
  it.skip = _it.skip;
  it.only = _it.only;
  // preserve "it.each" method if found
  // https://github.com/cypress-io/cypress-grep/issues/72
  if (typeof _it.each === "function") {
    it.each = _it.each;
  }

  describe.skip = _describe.skip;
  describe.only = _describe.only;
  if (typeof _describe.each === "function") {
    describe.each = _describe.each;
  }
}

function restartTests() {
  setTimeout(() => {
    window.top.document.querySelector(".reporter .restart").click();
  }, 0);
}

if (!Cypress.grep) {
  Cypress.grep = function grep(grep, tags, burn) {
    Cypress.env("grep", grep);
    Cypress.env("grepTags", tags);
    Cypress.env("grepBurn", burn);
    // remove any aliased values
    Cypress.env("grep-tags", null);
    Cypress.env("grep-burn", null);
    Cypress.env("burn", null);

    debug('set new grep to "%o" restarting tests', { grep, tags, burn });
    restartTests();
  };
}

module.exports = cypressGrep;
