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

const { BridgedEngine, BridgeWrapperXPCOM } = ChromeUtils.importESModule(
  "resource://services-sync/bridged_engine.sys.mjs"
);
const { Service } = ChromeUtils.importESModule(
  "resource://services-sync/service.sys.mjs"
);

// Wraps an `object` in a proxy so that its methods are bound to it. This
// simulates how XPCOM class instances have all their methods bound.
function withBoundMethods(object) {
  return new Proxy(object, {
    get(target, key) {
      let value = target[key];
      return typeof value == "function" ? value.bind(target) : value;
    },
  });
}

add_task(async function test_interface() {
  class TestBridge {
    constructor() {
      this.storageVersion = 2;
      this.syncID = "syncID111111";
      this.clear();
    }

    clear() {
      this.lastSyncMillis = 0;
      this.wasSyncStarted = false;
      this.incomingEnvelopes = [];
      this.uploadedIDs = [];
      this.wasSyncFinished = false;
      this.wasReset = false;
      this.wasWiped = false;
    }

    // `mozIBridgedSyncEngine` methods.

    getLastSync(callback) {
      CommonUtils.nextTick(() => callback.handleSuccess(this.lastSyncMillis));
    }

    setLastSync(millis, callback) {
      this.lastSyncMillis = millis;
      CommonUtils.nextTick(() => callback.handleSuccess());
    }

    resetSyncId(callback) {
      CommonUtils.nextTick(() => callback.handleSuccess(this.syncID));
    }

    ensureCurrentSyncId(newSyncId, callback) {
      equal(newSyncId, this.syncID, "Local and new sync IDs should match");
      CommonUtils.nextTick(() => callback.handleSuccess(this.syncID));
    }

    syncStarted(callback) {
      this.wasSyncStarted = true;
      CommonUtils.nextTick(() => callback.handleSuccess());
    }

    storeIncoming(envelopes, callback) {
      this.incomingEnvelopes.push(...envelopes.map(r => JSON.parse(r)));
      CommonUtils.nextTick(() => callback.handleSuccess());
    }

    apply(callback) {
      let outgoingEnvelopes = [
        {
          id: "hanson",
          data: {
            plants: ["seed", "flower 💐", "rose"],
            canYouTell: false,
          },
        },
        {
          id: "sheryl-crow",
          data: {
            today: "winding 🛣",
            tomorrow: "winding 🛣",
          },
        },
      ].map(cleartext =>
        JSON.stringify({
          id: cleartext.id,
          payload: JSON.stringify(cleartext),
        })
      );
      CommonUtils.nextTick(() => callback.handleSuccess(outgoingEnvelopes));
    }

    setUploaded(millis, ids, callback) {
      this.uploadedIDs.push(...ids);
      CommonUtils.nextTick(() => callback.handleSuccess());
    }

    syncFinished(callback) {
      this.wasSyncFinished = true;
      CommonUtils.nextTick(() => callback.handleSuccess());
    }

    reset(callback) {
      this.clear();
      this.wasReset = true;
      CommonUtils.nextTick(() => callback.handleSuccess());
    }

    wipe(callback) {
      this.clear();
      this.wasWiped = true;
      CommonUtils.nextTick(() => callback.handleSuccess());
    }
  }

  let bridge = new TestBridge();
  let engine = new BridgedEngine("Nineties", Service);
  engine._bridge = new BridgeWrapperXPCOM(withBoundMethods(bridge));
  engine.enabled = true;

  let server = await serverForFoo(engine);
  try {
    await SyncTestingInfrastructure(server);

    info("Add server records");
    let foo = server.user("foo");
    let collection = foo.collection("nineties");
    let now = new_timestamp();
    collection.insert(
      "backstreet",
      encryptPayload({
        id: "backstreet",
        data: {
          say: "I want it that way",
          when: "never",
        },
      }),
      now
    );
    collection.insert(
      "tlc",
      encryptPayload({
        id: "tlc",
        data: {
          forbidden: ["scrubs 🚫"],
          numberAvailable: false,
        },
      }),
      now + 5
    );

    info("Sync the engine");
    // Advance the last sync time to skip the Backstreet Boys...
    bridge.lastSyncMillis = 1000 * (now + 2);
    await sync_engine_and_validate_telem(engine, false);

    let metaGlobal = foo.collection("meta").wbo("global").get();
    deepEqual(
      JSON.parse(metaGlobal.payload).engines.nineties,
      {
        version: 2,
        syncID: "syncID111111",
      },
      "Should write storage version and sync ID to m/g"
    );

    greater(bridge.lastSyncMillis, 0, "Should update last sync time");
    ok(
      bridge.wasSyncStarted,
      "Should have started sync before storing incoming"
    );
    deepEqual(
      bridge.incomingEnvelopes
        .sort((a, b) => a.id.localeCompare(b.id))
        .map(({ payload, ...envelope }) => ({
          cleartextAsObject: JSON.parse(payload),
          ...envelope,
        })),
      [
        {
          id: "tlc",
          modified: now + 5,
          cleartextAsObject: {
            id: "tlc",
            data: {
              forbidden: ["scrubs 🚫"],
              numberAvailable: false,
            },
          },
        },
      ],
      "Should stage incoming records from server"
    );
    deepEqual(
      bridge.uploadedIDs.sort(),
      ["hanson", "sheryl-crow"],
      "Should mark new local records as uploaded"
    );
    ok(bridge.wasSyncFinished, "Should have finished sync after uploading");

    deepEqual(
      collection.keys().sort(),
      ["backstreet", "hanson", "sheryl-crow", "tlc"],
      "Should have all records on server"
    );
    let expectedRecords = [
      {
        id: "sheryl-crow",
        data: {
          today: "winding 🛣",
          tomorrow: "winding 🛣",
        },
      },
      {
        id: "hanson",
        data: {
          plants: ["seed", "flower 💐", "rose"],
          canYouTell: false,
        },
      },
    ];
    for (let expected of expectedRecords) {
      let actual = collection.cleartext(expected.id);
      deepEqual(
        actual,
        expected,
        `Should upload record ${expected.id} from bridged engine`
      );
    }

    await engine.resetClient();
    ok(bridge.wasReset, "Should reset local storage for bridge");

    await engine.wipeClient();
    ok(bridge.wasWiped, "Should wipe local storage for bridge");

    await engine.resetSyncID();
    ok(
      !foo.collection("nineties"),
      "Should delete server collection after resetting sync ID"
    );
  } finally {
    await promiseStopServer(server);
    await engine.finalize();
  }
});
