/* Any copyright is dedicated to the Public Domain.
   https://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { LoginTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/LoginTestUtils.sys.mjs"
);

const { LoginBreaches } = ChromeUtils.importESModule(
  "resource:///modules/LoginBreaches.sys.mjs"
);

const { RemoteSettings } = ChromeUtils.importESModule(
  "resource://services-settings/remote-settings.sys.mjs"
);

const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
);

const { OSKeyStore } = ChromeUtils.importESModule(
  "resource://gre/modules/OSKeyStore.sys.mjs"
);

const nsLoginInfo = new Components.Constructor(
  "@mozilla.org/login-manager/loginInfo;1",
  Ci.nsILoginInfo,
  "init"
);

const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
  Ci.nsIObserver
);

ChromeUtils.defineESModuleGetters(this, {
  LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
});

const BREACH_EXAMPLE = {
  AddedDate: "2018-12-20T23:56:26Z",
  BreachDate: "2018-12-16",
  Domain: "breached.com",
  Name: "breached",
  PwnCount: 1643100,
  DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
  _status: "synced",
  id: "047940fe-d2fd-4314-b636-b4a952ee0044",
  last_modified: "1541615610052",
  schema: "1541615609018",
};

const BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
  origin: "https://www.breached.com",
  formActionOrigin: "https://www.breached.com",
  username: "username",
  password: "password",
  timePasswordChanged: new Date("2018-12-15").getTime(),
});

const TEST_LOGIN_1 = LoginTestUtils.testData.formLogin({
  username: "bob",
  password: "pass1",
  origin: "https://example1.com",
});

const TEST_LOGIN_2 = LoginTestUtils.testData.formLogin({
  username: "sally",
  password: "pass2",
  origin: "https://example2.com",
});

const TEST_LOGIN_3 = LoginTestUtils.testData.formLogin({
  username: "ned",
  password: "pass3",
  origin: "https://example3.com",
});

const DEFAULT_MOCK_LOGINS = [TEST_LOGIN_1, TEST_LOGIN_2, TEST_LOGIN_3];

class MockView {
  #snapshots = [];

  get snapshots() {
    return this.#snapshots;
  }

  set snapshots(snapshots) {
    this.#snapshots = snapshots;
  }

  messageFromViewModel(message, args) {
    const functionName = `receive${message}`;
    this[functionName]?.(args);

    info(`${functionName} message received by view.`);
  }

  async receiveShowSnapshots({ snapshots }) {
    this.snapshots = snapshots;
  }
}

async function addMockPasswords() {
  info("Adding mock passwords");

  for (let login of DEFAULT_MOCK_LOGINS) {
    info(`Saving login: ${login.username}, ${login.password}, ${login.origin}`);
    await LoginTestUtils.addLogin(login);
  }
}

async function addBreach() {
  info("Adding breach");
  async function emitSync() {
    await RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).emit(
      "sync",
      { data: { current: [BREACH_EXAMPLE] } }
    );
  }

  gBrowserGlue.observe(null, "browser-glue-test", "add-breaches-sync-handler");
  const db = RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).db;
  await db.importChanges({}, Date.now(), [BREACH_EXAMPLE]);
  await emitSync();
}

async function openPasswordsSidebar(aWindow = window) {
  info("Open Passwords sidebar");

  await aWindow.SidebarController.show("viewCPMSidebar");
  const sidebar = aWindow.document.getElementById("sidebar");
  await TestUtils.waitForCondition(
    () => sidebar.contentDocument.querySelector("megalist-alpha")?.shadowRoot
  );
  const megalist =
    sidebar.contentDocument.querySelector("megalist-alpha").shadowRoot;
  return megalist;
}

async function checkAllLoginsRendered(megalist) {
  info("Check that all logins are rendered.");
  const logins = await Services.logins.getAllLogins();
  await TestUtils.waitForCondition(() => {
    const passwordsList = megalist.querySelector(".passwords-list");
    return (
      passwordsList?.querySelectorAll("password-card").length === logins.length
    );
  }, "Password cards failed to render");

  ok(true, `${logins.length} password cards are rendered.`);
}

async function addNavigableOriginLogin() {
  LoginTestUtils.addLogin({
    username: "john",
    password: "pass4",
    origin: "https://example.com",
  });
}

async function ensureNoNotifications(megalist, notificationId) {
  info(`Ensure no notification with id ${notificationId} is rendered.`);
  await new Promise(resolve => setTimeout(resolve, 1000));

  const notifyMsgBars = Array.from(
    megalist.querySelectorAll("notification-message-bar")
  );
  notifyMsgBars.forEach(notifyMsgBar => {
    info(`Notification: ${notifyMsgBar.notification.id}`);
  });

  const notification = notifyMsgBars?.find(
    notifyMsgBar => notifyMsgBar.notification.id === notificationId
  );
  ok(!notification, `Notification with id ${notificationId} should not exist.`);
}

function waitForNotification(megalist, notificationId) {
  info(`Wait for notification with id ${notificationId}.`);
  const notifcationPromise = TestUtils.waitForCondition(() => {
    const notifMsgBars = Array.from(
      megalist.querySelectorAll("notification-message-bar")
    );
    return notifMsgBars?.find(
      notifMsgBar => notifMsgBar.notification.id === notificationId
    );
  }, `Notification with id ${notificationId} did not render.`);
  return notifcationPromise;
}

async function checkNotificationAndTelemetry(
  megalist,
  notificationId,
  eventIndex = 0
) {
  const notificationMessageBar = await waitForNotification(
    megalist,
    notificationId
  );
  const events = Glean.contextualManager.notificationShown.testGetValue();
  assertCPMGleanEvent(events[eventIndex], {
    notification_detail: notificationId.replaceAll("-", "_"),
  });
  return notificationMessageBar;
}

async function checkNotificationInteractionTelemetry(
  notifMsgBar,
  buttonId,
  expectedEvent,
  eventIndex = 0
) {
  info(`Click on ${buttonId} button and test telemetry.`);
  const mozMessageBar = notifMsgBar.shadowRoot.querySelector("moz-message-bar");
  const element = mozMessageBar.querySelector(`#${buttonId}`);
  element.click();
  const events = Glean.contextualManager.notificationInteraction.testGetValue();
  assertCPMGleanEvent(events[eventIndex], expectedEvent);
}

function setInputValue(loginForm, fieldElement, value) {
  info(`Filling ${fieldElement} with value '${value}'.`);
  const field = loginForm.shadowRoot.querySelector(fieldElement);
  field.inputEl.value = value;
  field.inputEl.dispatchEvent(
    new InputEvent("input", {
      composed: true,
      bubbles: true,
    })
  );
}

function getMegalistParent() {
  const megalistChromeWindow = gBrowser.documentGlobal[0];
  return megalistChromeWindow.browsingContext.currentWindowGlobal.getActor(
    "Megalist"
  );
}

async function waitForReauth(callBackFn) {
  const authExpirationTime = getMegalistParent().authExpirationTime();
  let reauthObserved = Promise.resolve();
  // FIXME: we still wait for reauth event even if OS auth not enabled.
  const isOSAuthEnabled = Services.prefs.getBoolPref(
    "signon.management.page.os-auth.locked.enabled",
    false
  );

  if (
    isOSAuthEnabled &&
    OSKeyStore.canReauth() &&
    Date.now() > authExpirationTime
  ) {
    info("Can reauth");
    reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
  }
  await callBackFn();

  info("Waiting for reauth event");
  return reauthObserved;
}

function waitForPasswordReveal(passwordLine) {
  const revealBtnPromise = BrowserTestUtils.waitForMutationCondition(
    passwordLine.loginLine,
    {
      attributeFilter: ["inputtype"],
    },
    () => passwordLine.loginLine.getAttribute("inputtype") === "text"
  );
  info("click on reveal button");
  passwordLine.revealBtn.click();
  return revealBtnPromise;
}

function waitForSnapshots() {
  info("Wait for headers.");
  const sidebar = document.getElementById("sidebar");
  const megalistComponent =
    sidebar.contentDocument.querySelector("megalist-alpha");
  return TestUtils.waitForCondition(
    () => megalistComponent.header,
    "Megalist header loaded."
  );
}

async function checkEmptyState(selector, megalist) {
  return await TestUtils.waitForCondition(() => {
    const emptyStateCard = megalist.querySelector(".empty-state-card");
    return !!emptyStateCard?.querySelector(selector);
  }, "Empty state card failed to render");
}

function assertCPMGleanEvent(actualEvent, expectedEvent) {
  info("Asserting CPM Glean event");

  for (let key of Object.keys(expectedEvent)) {
    Assert.equal(
      actualEvent.extra[key],
      expectedEvent[key],
      `${actualEvent.extra[key]} is the recorded ${key}.
        Expected: '${expectedEvent[key]}'.`
    );
  }
}

async function resetTelemetryIfKeyStoreTestable() {
  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
    ok(true, "Cannot test OSAuth.");
    return false;
  }
  Services.fog.testResetFOG();
  await Services.fog.testFlushAllChildren();
  return true;
}

function mockServicePrompt(buttonClicked = 0, checked = true) {
  return {
    async asyncConfirmEx(
      browsingContext,
      win,
      title,
      message,
      _flags,
      _button0,
      _button1,
      _button2,
      _checkLabel,
      _checkValue,
      _extraArgs
    ) {
      info(`Prompt title ${title}`);
      info(`Prompt message ${message}`);
      return {
        // eslint-disable-next-line mozilla/use-chromeutils-generateqi
        QueryInterface() {
          const propBag = new Map();
          propBag.set("buttonNumClicked", buttonClicked);
          propBag.set("checked", checked);
          return propBag;
        },
      };
    },
  };
}
