import {
  applyPretender,
  exists,
  resetSite,
  testCleanup,
  testsInitialized,
  testsTornDown,
} from "discourse/tests/helpers/qunit-helpers";
import pretender, {
  applyDefaultHandlers,
  pretenderHelpers,
  resetPretender,
} from "discourse/tests/helpers/create-pretender";
import { resetSettings } from "discourse/tests/helpers/site-settings";
import { getOwner, setDefaultOwner } from "discourse-common/lib/get-owner";
import {
  getSettledState,
  isSettled,
  setApplication,
  setResolver,
} from "@ember/test-helpers";
import { setupS3CDN, setupURL } from "discourse-common/lib/get-url";
import Application from "../app";
import MessageBus from "message-bus-client";
import PreloadStore from "discourse/lib/preload-store";
import { resetSettings as resetThemeSettings } from "discourse/lib/theme-settings-store";
import QUnit from "qunit";
import { ScrollingDOMMethods } from "discourse/mixins/scrolling";
import Session from "discourse/models/session";
import User from "discourse/models/user";
import bootbox from "bootbox";
import { buildResolver } from "discourse-common/resolver";
import deprecated from "discourse-common/lib/deprecated";
import { flushMap } from "discourse/services/store";
import sinon from "sinon";
import { disableCloaking } from "discourse/widgets/post-stream";
import { clearState as clearPresenceState } from "discourse/tests/helpers/presence-pretender";
import { addModuleExcludeMatcher } from "ember-cli-test-loader/test-support/index";
import SiteSettingService from "discourse/services/site-settings";
import jQuery from "jquery";
import { setupDeprecationCounter } from "discourse/tests/helpers/deprecation-counter";

const Plugin = $.fn.modal;
const Modal = Plugin.Constructor;

function AcceptanceModal(option, _relatedTarget) {
  return this.each(function () {
    let $this = $(this);
    let data = $this.data("bs.modal");
    let options = Object.assign(
      {},
      Modal.DEFAULTS,
      $this.data(),
      typeof option === "object" && option
    );

    if (!data) {
      $this.data("bs.modal", (data = new Modal(this, options)));
    }
    data.$body = $("#ember-testing");

    if (typeof option === "string") {
      data[option](_relatedTarget);
    } else if (options.show) {
      data.show(_relatedTarget);
    }
  });
}

let started = false;

function createApplication(config, settings) {
  const app = Application.create(config);

  app.injectTestHelpers();
  setApplication(app);
  setResolver(buildResolver("discourse").create({ namespace: app }));

  // Modern Ember only sets up a container when the ApplicationInstance
  // is booted. We have legacy code which relies on having access to a container
  // before boot (e.g. during pre-initializers)
  //
  // This hack sets up a container early, then stubs the container setup method
  // so that Ember will use the same container instance when it boots the ApplicationInstance
  //
  // Note that this hack is not required in production because we use the default `autoboot` flag,
  // which triggers the internal `_globalsMode` flag, which sets up an ApplicationInstance immediately when
  // an Application is initialized (via the `_buildDeprecatedInstance` method).
  //
  // In the future, we should move away from relying on the `container` before the ApplicationInstance
  // is booted, and then remove this hack.
  let container = app.__registry__.container();
  app.__container__ = container;
  setDefaultOwner(container);
  sinon
    .stub(Object.getPrototypeOf(app.__registry__), "container")
    .callsFake((opts) => {
      container.owner = opts.owner;
      container.registry = opts.owner.__registry__;
      return container;
    });

  SiteSettingService.create = () => settings;

  if (!started) {
    app.instanceInitializer({
      name: "test-helper",
      initialize: testsInitialized,
      teardown: testsTornDown,
    });

    app.start();
    started = true;
  }

  return app;
}

function setupToolbar() {
  // Most default toolbar items aren't useful for Discourse
  QUnit.config.urlConfig = QUnit.config.urlConfig.reject((c) =>
    ["noglobals", "nolint", "devmode", "dockcontainer", "nocontainer"].includes(
      c.id
    )
  );

  QUnit.config.urlConfig.push({
    id: "qunit_skip_core",
    label: "Skip Core",
    value: "1",
  });

  QUnit.config.urlConfig.push({
    id: "qunit_skip_plugins",
    label: "Skip Plugins",
    value: "1",
  });

  const pluginNames = new Set();

  Object.keys(requirejs.entries).forEach((moduleName) => {
    const found = moduleName.match(/\/plugins\/([\w-]+)\//);
    if (found && moduleName.match(/\-test/)) {
      pluginNames.add(found[1]);
    }
  });

  QUnit.config.urlConfig.push({
    id: "qunit_single_plugin",
    label: "Plugin",
    value: Array.from(pluginNames),
  });

  // Abort tests when the qunit controls are clicked
  document.querySelector("#qunit").addEventListener("click", ({ target }) => {
    if (!target.closest("#qunit-testrunner-toolbar")) {
      // Outside toolbar, carry on
      return;
    }

    if (target.closest("label[for=qunit-urlconfig-hidepassed]")) {
      // This one can be toggled during tests, carry on
      return;
    }

    if (["INPUT", "SELECT", "LABEL"].includes(target.tagName)) {
      document.querySelector("#qunit-abort-tests-button")?.click();
    }
  });
}

function reportMemoryUsageAfterTests() {
  QUnit.done(() => {
    const usageBytes = performance.memory?.usedJSHeapSize;
    let result;
    if (usageBytes) {
      result = `${(usageBytes / Math.pow(2, 30)).toFixed(3)}GB`;
    } else {
      result = "(performance.memory api unavailable)";
    }

    writeSummaryLine(`Used JS Heap Size: ${result}`);
  });
}

function writeSummaryLine(message) {
  // eslint-disable-next-line no-console
  console.log(`\n${message}\n`);
  if (window.Testem) {
    window.Testem.useCustomAdapter(function (socket) {
      socket.emit("test-metadata", "summary-line", {
        message,
      });
    });
  }
}

export default function setupTests(config) {
  disableCloaking();

  setupDeprecationCounter(QUnit);

  QUnit.config.hidepassed = true;

  sinon.config = {
    injectIntoThis: false,
    injectInto: null,
    properties: ["spy", "stub", "mock", "clock", "sandbox"],
    useFakeTimers: true,
    useFakeServer: false,
  };

  // Stop the message bus so we don't get ajax calls
  MessageBus.stop();

  // disable logster error reporting
  if (window.Logster) {
    window.Logster.enabled = false;
  } else {
    window.Logster = { enabled: false };
  }

  $.fn.modal = AcceptanceModal;

  Object.defineProperty(window, "exists", {
    get() {
      deprecated(
        "Accessing the global function `exists` is deprecated. Import it instead.",
        {
          since: "2.6.0.beta.4",
          dropFrom: "2.6.0",
          id: "discourse.qunit.global-exists",
        }
      );
      return exists;
    },
  });

  let setupData;
  const setupDataElement = document.getElementById("data-discourse-setup");
  if (setupDataElement) {
    setupData = setupDataElement.dataset;
    setupDataElement.remove();
  }

  let app;
  QUnit.testStart(function (ctx) {
    bootbox.$body = $("#ember-testing");
    let settings = resetSettings();
    resetThemeSettings();

    app = createApplication(config, settings);

    const cdn = setupData ? setupData.cdn : null;
    const baseUri = setupData ? setupData.baseUri : "";
    setupURL(cdn, "http://localhost:3000", baseUri, { snapshot: true });
    if (setupData && setupData.s3BaseUrl) {
      setupS3CDN(setupData.s3BaseUrl, setupData.s3Cdn, { snapshot: true });
    } else {
      setupS3CDN(null, null, { snapshot: true });
    }

    applyDefaultHandlers(pretender);

    pretender.prepareBody = function (body) {
      if (typeof body === "object") {
        return JSON.stringify(body);
      }
      return body;
    };

    if (QUnit.config.logAllRequests) {
      pretender.handledRequest = function (verb, path) {
        // eslint-disable-next-line no-console
        console.log("REQ: " + verb + " " + path);
      };
    }

    pretender.unhandledRequest = function (verb, path) {
      if (QUnit.config.logAllRequests) {
        // eslint-disable-next-line no-console
        console.log("REQ: " + verb + " " + path + " missing");
      }

      const error =
        "Unhandled request in test environment: " + path + " (" + verb + ")";

      // eslint-disable-next-line no-console
      console.error(error);
      throw new Error(error);
    };

    pretender.checkPassthrough = (request) =>
      request.requestHeaders["Discourse-Script"];

    applyPretender(ctx.module, pretender, pretenderHelpers());

    Session.resetCurrent();
    if (setupData) {
      const session = Session.current();
      session.markdownItURL = setupData.markdownItUrl;
      session.highlightJsPath = setupData.highlightJsPath;
    }
    User.resetCurrent();

    PreloadStore.reset();
    resetSite();

    sinon.stub(ScrollingDOMMethods, "screenNotFull");
    sinon.stub(ScrollingDOMMethods, "bindOnScroll");
    sinon.stub(ScrollingDOMMethods, "unbindOnScroll");
  });

  QUnit.testDone(function () {
    testCleanup(getOwner(app), app);

    sinon.restore();
    resetPretender();
    clearPresenceState();

    // Clean up the DOM. Some tests might leave extra classes or elements behind.
    Array.from(document.getElementsByClassName("modal-backdrop")).forEach((e) =>
      e.remove()
    );
    document.body.removeAttribute("class");
    let html = document.documentElement;
    html.removeAttribute("class");
    html.removeAttribute("style");
    let testing = document.getElementById("ember-testing");
    testing.removeAttribute("class");
    testing.removeAttribute("style");

    const testContainer = document.getElementById("ember-testing-container");
    testContainer.scrollTop = 0;
    testContainer.scrollLeft = 0;

    flushMap();

    MessageBus.unsubscribe("*");
    localStorage.clear();
  });

  if (getUrlParameter("qunit_disable_auto_start") === "1") {
    QUnit.config.autostart = false;
  }

  let skipCore =
    getUrlParameter("qunit_single_plugin") ||
    getUrlParameter("qunit_skip_core") === "1";

  let singlePlugin = getUrlParameter("qunit_single_plugin");
  let skipPlugins = !singlePlugin && getUrlParameter("qunit_skip_plugins");

  if (skipCore && !getUrlParameter("qunit_skip_core")) {
    replaceUrlParameter("qunit_skip_core", "1");
  }

  if (!skipPlugins && getUrlParameter("qunit_skip_plugins")) {
    replaceUrlParameter("qunit_skip_plugins", null);
  }

  const shouldLoadModule = (name) => {
    if (!/\-test/.test(name)) {
      return false;
    }

    const isPlugin = name.match(/\/plugins\//);
    const isCore = !isPlugin;
    const pluginName = name.match(/\/plugins\/([\w-]+)\//)?.[1];

    if (skipCore && isCore) {
      return false;
    } else if (skipPlugins && isPlugin) {
      return false;
    } else if (singlePlugin && singlePlugin !== pluginName) {
      return false;
    }
    return true;
  };

  addModuleExcludeMatcher((name) => !shouldLoadModule(name));

  // forces 0 as duration for all jquery animations
  jQuery.fx.off = true;

  setupToolbar();
  reportMemoryUsageAfterTests();
  patchFailedAssertion();
}

function getUrlParameter(name) {
  const queryParams = new URLSearchParams(window.location.search);
  return queryParams.get(name);
}

function replaceUrlParameter(name, value) {
  const queryParams = new URLSearchParams(window.location.search);
  if (value === null) {
    queryParams.delete(name);
  } else {
    queryParams.set(name, value);
  }
  history.replaceState(null, null, "?" + queryParams.toString());

  QUnit.begin(() => {
    QUnit.config[name] = value;
    const formElement = document.querySelector(
      `#qunit-testrunner-toolbar [name=${name}]`
    );
    if (formElement?.type === "checkbox") {
      formElement.checked = !!value;
    } else if (formElement) {
      formElement.value = value;
    }
  });
}

function patchFailedAssertion() {
  const oldPushResult = QUnit.assert.pushResult;

  QUnit.assert.pushResult = function (resultInfo) {
    if (!resultInfo.result && !isSettled()) {
      // eslint-disable-next-line no-console
      console.warn(
        "ℹ️ Hint: when the assertion failed, the Ember runloop was not in a settled state. Maybe you missed an `await` further up the test? Or maybe you need to manually add `await settled()` before your assertion?",
        getSettledState()
      );
    }

    oldPushResult.call(this, resultInfo);
  };
}
