/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

import { CommonUtils } from "resource://services-common/utils.sys.mjs";

import { Assert } from "resource://testing-common/Assert.sys.mjs";

import { initTestLogging } from "resource://testing-common/services/common/logging.sys.mjs";
import {
  FakeCryptoService,
  FakeFilesystemService,
  FakeGUIDService,
  fakeSHA256HMAC,
} from "resource://testing-common/services/sync/fakeservices.sys.mjs";

import {
  FxAccounts,
  AccountState,
} from "resource://gre/modules/FxAccounts.sys.mjs";
import { FxAccountsClient } from "resource://gre/modules/FxAccountsClient.sys.mjs";

import { SCOPE_OLD_SYNC } from "resource://gre/modules/FxAccountsCommon.sys.mjs";

// A mock "storage manager" for FxAccounts that doesn't actually write anywhere.
export function MockFxaStorageManager() {}

MockFxaStorageManager.prototype = {
  promiseInitialized: Promise.resolve(),

  initialize(accountData) {
    this.accountData = accountData;
  },

  finalize() {
    return Promise.resolve();
  },

  getAccountData(fields = null) {
    let result;
    if (!this.accountData) {
      result = null;
    } else if (fields == null) {
      // can't use cloneInto as the keys get upset...
      result = {};
      for (let field of Object.keys(this.accountData)) {
        result[field] = this.accountData[field];
      }
    } else {
      if (!Array.isArray(fields)) {
        fields = [fields];
      }
      result = {};
      for (let field of fields) {
        result[field] = this.accountData[field];
      }
    }
    return Promise.resolve(result);
  },

  updateAccountData(updatedFields) {
    for (let [name, value] of Object.entries(updatedFields)) {
      if (value == null) {
        delete this.accountData[name];
      } else {
        this.accountData[name] = value;
      }
    }
    return Promise.resolve();
  },

  deleteAccountData() {
    this.accountData = null;
    return Promise.resolve();
  },
};

/**
 * First wait >100ms (nsITimers can take up to that much time to fire, so
 * we can account for the timer in delayedAutoconnect) and then two event
 * loop ticks (to account for the CommonUtils.nextTick() in autoConnect).
 */
export function waitForZeroTimer(callback) {
  let ticks = 2;
  function wait() {
    if (ticks) {
      ticks -= 1;
      CommonUtils.nextTick(wait);
      return;
    }
    callback();
  }
  CommonUtils.namedTimer(wait, 150, {}, "timer");
}

export var promiseZeroTimer = function () {
  return new Promise(resolve => {
    waitForZeroTimer(resolve);
  });
};

export var promiseNamedTimer = function (wait, thisObj, name) {
  return new Promise(resolve => {
    CommonUtils.namedTimer(resolve, wait, thisObj, name);
  });
};

// Return an identity configuration suitable for testing with our identity
// providers.  |overrides| can specify overrides for any default values.
// |server| is optional, but if specified, will be used to form the cluster
// URL for the FxA identity.
export var makeIdentityConfig = function (overrides) {
  // first setup the defaults.
  let result = {
    // Username used in both fxaccount and sync identity configs.
    username: "foo",
    // fxaccount specific credentials.
    fxaccount: {
      user: {
        email: "foo",
        scopedKeys: {
          [SCOPE_OLD_SYNC]: {
            kid: "1234567890123-u7u7u7u7u7u7u7u7u7u7uw",
            k: "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg",
            kty: "oct",
          },
        },
        sessionToken: "sessionToken",
        uid: "a".repeat(32),
        verified: true,
      },
      token: {
        endpoint: null,
        duration: 300,
        id: "id",
        key: "key",
        hashed_fxa_uid: "f".repeat(32), // used during telemetry validation
        // uid will be set to the username.
      },
    },
  };

  // Now handle any specified overrides.
  if (overrides) {
    if (overrides.username) {
      result.username = overrides.username;
    }
    if (overrides.fxaccount) {
      // TODO: allow just some attributes to be specified
      result.fxaccount = overrides.fxaccount;
    }
    if (overrides.node_type) {
      result.fxaccount.token.node_type = overrides.node_type;
    }
  }
  return result;
};

export var makeFxAccountsInternalMock = function (config) {
  return {
    newAccountState(credentials) {
      // We only expect this to be called with null indicating the (mock)
      // storage should be read.
      if (credentials) {
        throw new Error("Not expecting to have credentials passed");
      }
      let storageManager = new MockFxaStorageManager();
      storageManager.initialize(config.fxaccount.user);
      let accountState = new AccountState(storageManager);
      return accountState;
    },
    getOAuthToken: () => Promise.resolve("some-access-token"),
    _destroyOAuthToken: () => Promise.resolve(),
    keys: {
      getScopedKeys: () =>
        Promise.resolve({
          "https://identity.mozilla.com/apps/oldsync": {
            identifier: "https://identity.mozilla.com/apps/oldsync",
            keyRotationSecret:
              "0000000000000000000000000000000000000000000000000000000000000000",
            keyRotationTimestamp: 1510726317123,
          },
        }),
    },
    profile: {
      getProfile() {
        return null;
      },
    },
  };
};

// Configure an instance of an FxAccount identity provider with the specified
// config (or the default config if not specified).
export var configureFxAccountIdentity = function (
  authService,
  config = makeIdentityConfig(),
  fxaInternal = makeFxAccountsInternalMock(config)
) {
  // until we get better test infrastructure for bid_identity, we set the
  // signedin user's "email" to the username, simply as many tests rely on this.
  config.fxaccount.user.email = config.username;

  let fxa = new FxAccounts(fxaInternal);

  let MockFxAccountsClient = function () {
    FxAccountsClient.apply(this);
  };
  MockFxAccountsClient.prototype = {
    accountStatus() {
      return Promise.resolve(true);
    },
  };
  Object.setPrototypeOf(
    MockFxAccountsClient.prototype,
    FxAccountsClient.prototype
  );
  let mockFxAClient = new MockFxAccountsClient();
  fxa._internal._fxAccountsClient = mockFxAClient;

  let mockTSC = {
    // TokenServerClient
    async getTokenUsingOAuth(url, oauthToken) {
      Assert.equal(
        url,
        Services.prefs.getStringPref("identity.sync.tokenserver.uri")
      );
      Assert.ok(oauthToken, "oauth token present");
      config.fxaccount.token.uid = config.username;
      return config.fxaccount.token;
    },
  };
  authService._fxaService = fxa;
  authService._tokenServerClient = mockTSC;
  // Set the "account" of the sync auth manager to be the "email" of the
  // logged in user of the mockFXA service.
  authService._signedInUser = config.fxaccount.user;
  authService._account = config.fxaccount.user.email;
};

export var configureIdentity = async function (identityOverrides, server) {
  let config = makeIdentityConfig(identityOverrides, server);
  // Must be imported after the identity configuration is set up.
  let { Service } = ChromeUtils.importESModule(
    "resource://services-sync/service.sys.mjs"
  );

  // If a server was specified, ensure FxA has a correct cluster URL available.
  if (server && !config.fxaccount.token.endpoint) {
    let ep = server.baseURI;
    if (!ep.endsWith("/")) {
      ep += "/";
    }
    ep += "1.1/" + config.username + "/";
    config.fxaccount.token.endpoint = ep;
  }

  configureFxAccountIdentity(Service.identity, config);
  Services.prefs.setStringPref("services.sync.username", config.username);
  // many of these tests assume all the auth stuff is setup and don't hit
  // a path which causes that auth to magically happen - so do it now.
  await Service.identity._ensureValidToken();

  // and cheat to avoid requiring each test do an explicit login - give it
  // a cluster URL.
  if (config.fxaccount.token.endpoint) {
    Service.clusterURL = config.fxaccount.token.endpoint;
  }
};

export function syncTestLogging(level = "Trace") {
  let logStats = initTestLogging(level);
  Services.prefs.setStringPref("services.sync.log.logger", level);
  Services.prefs.setStringPref("services.sync.log.logger.engine", "");
  return logStats;
}

export var SyncTestingInfrastructure = async function (server, username) {
  let config = makeIdentityConfig({ username });
  await configureIdentity(config, server);
  return {
    logStats: syncTestLogging(),
    fakeFilesystem: new FakeFilesystemService({}),
    fakeGUIDService: new FakeGUIDService(),
    fakeCryptoService: new FakeCryptoService(),
  };
};

/**
 * Turn WBO cleartext into fake "encrypted" payload as it goes over the wire.
 */
export function encryptPayload(cleartext) {
  if (typeof cleartext == "object") {
    cleartext = JSON.stringify(cleartext);
  }

  return {
    ciphertext: cleartext, // ciphertext == cleartext with fake crypto
    IV: "irrelevant",
    hmac: fakeSHA256HMAC(cleartext),
  };
}

export var sumHistogram = function (name, options = {}) {
  let histogram = options.key
    ? Services.telemetry.getKeyedHistogramById(name)
    : Services.telemetry.getHistogramById(name);
  let snapshot = histogram.snapshot();
  let sum = -Infinity;
  if (snapshot) {
    if (options.key && snapshot[options.key]) {
      sum = snapshot[options.key].sum;
    } else {
      sum = snapshot.sum;
    }
  }
  histogram.clear();
  return sum;
};
