import QUnit, { module, skip, test } from "qunit";
import { cloneJSON, deepMerge } from "discourse-common/lib/object";
import MessageBus from "message-bus-client";
import {
  clearCache as clearOutletCache,
  resetExtraClasses,
} from "discourse/lib/plugin-connectors";
import { clearRewrites } from "discourse/lib/url";
import {
  currentSettings,
  mergeSettings,
} from "discourse/tests/helpers/site-settings";
import { forceMobile, resetMobile } from "discourse/lib/mobile";
import { getApplication, settled } from "@ember/test-helpers";
import { getOwner } from "discourse-common/lib/get-owner";
import { run } from "@ember/runloop";
import { setupApplicationTest } from "ember-qunit";
import Site from "discourse/models/site";
import User from "discourse/models/user";
import { _clearSnapshots } from "select-kit/components/composer-actions";
import { clearHTMLCache } from "discourse/helpers/custom-html";
import deprecated from "discourse-common/lib/deprecated";
import { restoreBaseUri } from "discourse-common/lib/get-url";
import { initSearchData } from "discourse/widgets/search-menu";
import { resetPostMenuExtraButtons } from "discourse/widgets/post-menu";
import { isEmpty } from "@ember/utils";
import { resetCustomPostMessageCallbacks } from "discourse/controllers/topic";
import { resetDecorators } from "discourse/widgets/widget";
import { resetCache as resetOneboxCache } from "pretty-text/oneboxer";
import { resetDecorators as resetPluginOutletDecorators } from "discourse/components/plugin-connector";
import { resetDecorators as resetPostCookedDecorators } from "discourse/widgets/post-cooked";
import { resetTopicTitleDecorators } from "discourse/components/topic-title";
import { resetUsernameDecorators } from "discourse/helpers/decorate-username-selector";
import { resetWidgetCleanCallbacks } from "discourse/components/mount-widget";
import { resetUserSearchCache } from "discourse/lib/user-search";
import { resetCardClickListenerSelector } from "discourse/mixins/card-contents-base";
import { resetComposerCustomizations } from "discourse/models/composer";
import { resetQuickSearchRandomTips } from "discourse/widgets/search-menu-results";
import sessionFixtures from "discourse/tests/fixtures/session-fixtures";
import {
  resetHighestReadCache,
  setTopicList,
} from "discourse/lib/topic-list-tracker";
import sinon from "sinon";
import siteFixtures from "discourse/tests/fixtures/site-fixtures";
import { clearExtraKeyboardShortcutHelp } from "discourse/lib/keyboard-shortcuts";
import { clearResolverOptions } from "discourse-common/resolver";
import { clearResolverOptions as clearLegacyResolverOptions } from "discourse-common/lib/legacy-resolver";
import { clearNavItems } from "discourse/models/nav-item";
import {
  cleanUpComposerUploadHandler,
  cleanUpComposerUploadMarkdownResolver,
  cleanUpComposerUploadPreProcessor,
} from "discourse/components/composer-editor";
import { resetLastEditNotificationClick } from "discourse/models/post-stream";
import { clearAuthMethods } from "discourse/models/login-method";
import { clearTopicFooterDropdowns } from "discourse/lib/register-topic-footer-dropdown";
import { clearTopicFooterButtons } from "discourse/lib/register-topic-footer-button";
import { clearDesktopNotificationHandlers } from "discourse/lib/desktop-notifications";
import {
  clearPresenceCallbacks,
  setTestPresence,
} from "discourse/lib/user-presence";
import PreloadStore from "discourse/lib/preload-store";
import { resetDefaultSectionLinks as resetTopicsSectionLinks } from "discourse/lib/sidebar/custom-community-section-links";
import {
  clearBlockDecorateCallbacks,
  clearTagDecorateCallbacks,
  clearTextDecorateCallbacks,
} from "discourse/lib/to-markdown";
import { clearTagsHtmlCallbacks } from "discourse/lib/render-tags";
import { clearToolbarCallbacks } from "discourse/components/d-editor";
import { clearExtraHeaderIcons } from "discourse/widgets/header";
import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
import { resetMentions } from "discourse/lib/link-mentions";
import { resetModelTransformers } from "discourse/lib/model-transformers";
import { cleanupTemporaryModuleRegistrations } from "./temporary-module-helper";

export function currentUser() {
  return User.create(sessionFixtures["/session/current.json"].current_user);
}

let _initialized = new Set();

export function testsInitialized() {
  _initialized.add(QUnit.config.current.testId);
}

export function testsTornDown() {
  _initialized.delete(QUnit.config.current.testId);
}

export function updateCurrentUser(properties) {
  run(() => {
    User.current().setProperties(properties);
  });
}

// Note: do not use this in acceptance tests. Use `loggedIn: true` instead
export function logIn() {
  User.resetCurrent(currentUser());
}

// Note: Only use if `loggedIn: true` has been used in an acceptance test
export function loggedInUser() {
  return User.current();
}

export function fakeTime(timeString, timezone = null, advanceTime = false) {
  let now = moment.tz(timeString, timezone);
  return sinon.useFakeTimers({
    now: now.valueOf(),
    shouldAdvanceTime: advanceTime,
    shouldClearNativeTimers: true,
  });
}

export function withFrozenTime(timeString, timezone, callback) {
  const clock = fakeTime(timeString, timezone, false);
  try {
    callback();
  } finally {
    clock.restore();
  }
}

let _pretenderCallbacks = {};

export function resetSite(extras = {}) {
  const siteAttrs = {
    ...siteFixtures["site.json"].site,
    ...extras,
  };

  PreloadStore.store("site", cloneJSON(siteAttrs));
  Site.resetCurrent();
}

export function applyPretender(name, server, helper) {
  const cb = _pretenderCallbacks[name];
  if (cb) {
    cb(server, helper);
  }
}

// Add clean up code here to run after every test
export function testCleanup(container, app) {
  if (_initialized.has(QUnit.config.current.testId)) {
    if (!app) {
      app = getApplication();
    }
    app._runInitializer("instanceInitializers", (_, initializer) => {
      initializer.teardown?.();
    });

    app._runInitializer("initializers", (_, initializer) => {
      initializer.teardown?.(container);
    });
  }

  User.resetCurrent();
  resetExtraClasses();
  clearOutletCache();
  clearHTMLCache();
  clearRewrites();
  initSearchData();
  resetDecorators();
  resetPostCookedDecorators();
  resetPluginOutletDecorators();
  resetTopicTitleDecorators();
  resetUsernameDecorators();
  resetOneboxCache();
  resetCustomPostMessageCallbacks();
  resetUserSearchCache();
  resetHighestReadCache();
  resetCardClickListenerSelector();
  resetComposerCustomizations();
  resetQuickSearchRandomTips();
  resetPostMenuExtraButtons();
  clearExtraKeyboardShortcutHelp();
  clearNavItems();
  setTopicList(null);
  _clearSnapshots();
  cleanUpComposerUploadHandler();
  cleanUpComposerUploadMarkdownResolver();
  cleanUpComposerUploadPreProcessor();
  clearTopicFooterDropdowns();
  clearTopicFooterButtons();
  clearDesktopNotificationHandlers();
  resetLastEditNotificationClick();
  clearAuthMethods();
  setTestPresence(true);
  clearPresenceCallbacks();
  restoreBaseUri();
  resetTopicsSectionLinks();
  clearTagDecorateCallbacks();
  clearBlockDecorateCallbacks();
  clearTextDecorateCallbacks();
  clearResolverOptions();
  clearLegacyResolverOptions();
  clearTagsHtmlCallbacks();
  clearToolbarCallbacks();
  resetSidebarSection();
  resetNotificationTypeRenderers();
  clearExtraHeaderIcons();
  resetUserMenuTabs();
  resetLinkLookup();
  resetModelTransformers();
  resetMentions();
  cleanupTemporaryModuleRegistrations();
}

export function discourseModule(name, options) {
  // deprecated(
  //   `${name}: \`discourseModule\` is deprecated. Use QUnit's \`module\` instead.`,
  //   { since: "2.6.0" }
  // );

  if (typeof options === "function") {
    module(name, function (hooks) {
      hooks.beforeEach(function () {
        this.container = getOwner(this);
        this.registry = this.container.registry;
        this.owner = this.container;
        this.siteSettings = currentSettings();
      });

      this.getController = function (controllerName, properties) {
        let controller = this.container.lookup(`controller:${controllerName}`);
        controller.application = {};
        controller.siteSettings = this.siteSettings;
        if (properties) {
          controller.setProperties(properties);
        }
        return controller;
      };

      this.moduleName = name;

      hooks.usingDiscourseModule = true;
      options.call(this, hooks);
    });

    return;
  }

  module(name, {
    beforeEach() {
      this.container = getOwner(this);
      this.siteSettings = currentSettings();
      options?.beforeEach?.call(this);
    },
    afterEach() {
      options?.afterEach?.call(this);
    },
  });
}

export function addPretenderCallback(name, fn) {
  if (name && fn) {
    if (_pretenderCallbacks[name]) {
      throw `There is already a pretender callback with module name (${name}).`;
    }

    _pretenderCallbacks[name] = fn;
  }
}

export function acceptance(name, optionsOrCallback) {
  name = `Acceptance: ${name}`;

  let callback;
  let options = {};
  if (typeof optionsOrCallback === "function") {
    callback = optionsOrCallback;
  } else if (typeof optionsOrCallback === "object") {
    deprecated(
      `${name}: The second parameter to \`acceptance\` should be a function that encloses your tests.`,
      {
        since: "2.6.0",
        dropFrom: "2.9.0.beta1",
        id: "discourse.qunit.acceptance-function",
      }
    );
    options = optionsOrCallback;
  }

  addPretenderCallback(name, options.pretend);

  let loggedIn = false;
  let mobileView = false;
  let siteChanges;
  let settingChanges;
  let userChanges;

  const setup = {
    beforeEach() {
      resetMobile();

      resetExtraClasses();
      if (mobileView) {
        forceMobile();
      }

      if (loggedIn) {
        logIn();
        if (userChanges) {
          updateCurrentUser(userChanges);
        }

        User.current().appEvents = getOwner(this).lookup("service:appEvents");
        User.current().trackStatus();
      }

      if (settingChanges) {
        mergeSettings(settingChanges);
      }

      this.siteSettings = currentSettings();

      resetSite(siteChanges);

      this.container = getOwner(this);

      if (!this.owner) {
        this.owner = this.container;
      }

      if (options.beforeEach) {
        options.beforeEach.call(this);
      }
    },

    afterEach() {
      resetMobile();
      let app = getApplication();
      options?.afterEach?.call(this);
      if (loggedIn) {
        User.current().stopTrackingStatus();
      }
      testCleanup(this.container, app);

      // We do this after reset so that the willClearRender will have already fired
      resetWidgetCleanCallbacks();
    },
  };

  const needs = {
    user(changes) {
      loggedIn = true;
      userChanges = changes;
    },
    pretender(fn) {
      addPretenderCallback(name, fn);
    },
    site(changes) {
      siteChanges = changes;
    },
    settings(changes) {
      settingChanges = changes;
    },
    mobileView() {
      mobileView = true;
    },
  };

  if (options.loggedIn) {
    needs.user();
  }
  if (options.site) {
    needs.site(options.site);
  }
  if (options.settings) {
    needs.settings(options.settings);
  }
  if (options.mobileView) {
    needs.mobileView();
  }

  if (callback) {
    // New, preferred way
    module(name, function (hooks) {
      needs.hooks = hooks;
      hooks.beforeEach(setup.beforeEach);
      hooks.afterEach(setup.afterEach);
      callback(needs);

      setupApplicationTest(hooks);
    });
  } else {
    // Old way
    module(name, setup);
  }
}

export function controllerFor(controller, model) {
  deprecated(
    'controllerFor is deprecated. Use the standard `getOwner(this).lookup("controller:NAME")` instead',
    {
      id: "controller-for",
      since: "3.0.0.beta14",
    }
  );

  controller = getOwner(this).lookup("controller:" + controller);
  if (model) {
    controller.set("model", model);
  }
  return controller;
}

export function fixture(selector) {
  if (selector) {
    return document.querySelector(`#qunit-fixture ${selector}`);
  }
  return document.querySelector("#qunit-fixture");
}

QUnit.assert.not = function (actual, message) {
  deprecated("assert.not() is deprecated. Use assert.false() instead.", {
    since: "2.9.0.beta1",
    dropFrom: "2.10.0.beta1",
    id: "discourse.qunit.assert-not",
  });

  this.pushResult({
    result: !actual,
    actual,
    expected: !actual,
    message,
  });
};

QUnit.assert.blank = function (actual, message) {
  this.pushResult({
    result: isEmpty(actual),
    actual,
    message,
  });
};

QUnit.assert.present = function (actual, message) {
  this.pushResult({
    result: !isEmpty(actual),
    actual,
    message,
  });
};

QUnit.assert.containsInstance = function (collection, klass, message) {
  const result = klass.detectInstance(collection[0]);
  this.pushResult({
    result,
    message,
  });
};

export async function selectDate(selector, date) {
  const elem = document.querySelector(selector);
  elem.value = date;
  const evt = new Event("input", { bubbles: true, cancelable: false });
  elem.dispatchEvent(evt);
  elem.blur();
}

export function queryAll(selector, context) {
  context = context || "#ember-testing";
  return $(selector, context);
}

export function query() {
  return document.querySelector("#ember-testing").querySelector(...arguments);
}

export function invisible(selector) {
  const $items = queryAll(selector + ":visible");
  return (
    $items.length === 0 ||
    $items.css("opacity") !== "1" ||
    $items.css("visibility") === "hidden"
  );
}

export function visible(selector) {
  return queryAll(selector + ":visible").length > 0;
}

export function count(selector) {
  return queryAll(selector).length;
}

export function exists(selector) {
  return count(selector) > 0;
}

export async function publishToMessageBus(channelPath, ...args) {
  args = cloneJSON(args);

  const promises = MessageBus.callbacks
    .filterBy("channel", channelPath)
    .map((callback) => callback.func(...args));

  await Promise.allSettled(promises);
  await settled();
}

export async function selectText(selector, endOffset = null) {
  const range = document.createRange();
  let node;

  if (typeof selector === "string") {
    node = document.querySelector(selector);
  } else {
    node = selector;
  }

  range.selectNodeContents(node);

  if (endOffset) {
    range.setEnd(node, endOffset);
  }

  const performSelection = () => {
    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);
  };

  performSelection();

  await settled();
}

export function conditionalTest(name, condition, testCase) {
  if (condition) {
    test(name, testCase);
  } else {
    skip(name, testCase);
  }
}

export function chromeTest(name, testCase) {
  conditionalTest(name, navigator.userAgent.includes("Chrome"), testCase);
}

export function firefoxTest(name, testCase) {
  conditionalTest(name, navigator.userAgent.includes("Firefox"), testCase);
}

export function createFile(name, type = "image/png", blobData = null) {
  // the blob content doesn't matter at all, just want it to be random-ish
  blobData = blobData || (Math.random() + 1).toString(36).substring(2);
  const blob = new Blob([blobData]);
  const file = new File([blob], name, {
    type,
    lastModified: new Date().getTime(),
  });
  return file;
}

export async function paste(element, text, otherClipboardData = {}) {
  let e = new Event("paste", { cancelable: true });
  e.clipboardData = deepMerge({ getData: () => text }, otherClipboardData);
  element.dispatchEvent(e);
  await settled();
  return e;
}

// The order of attributes can vary in different browsers. When comparing
// HTML strings from the DOM, this function helps to normalize them to make
// comparison work cross-browser
export function normalizeHtml(html) {
  const resultElement = document.createElement("template");
  resultElement.innerHTML = html;
  return resultElement.innerHTML;
}
