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

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

add_task(async function test_transaction_in_progress() {
  let buf = await openMirror("transaction_in_progress");

  await storeRecords(buf, [
    {
      id: "menu",
      parentid: "places",
      type: "folder",
      children: ["bookmarkAAAA"],
    },
    {
      id: "bookmarkAAAA",
      parentid: "menu",
      type: "bookmark",
      title: "A",
      bmkUri: "http://example.com/a",
    },
  ]);

  // This transaction should block merging until the transaction is committed.
  info("Open transaction on Places connection");
  await buf.db.execute("BEGIN EXCLUSIVE");

  await Assert.rejects(
    buf.apply(),
    ex => ex.name == "MergeConflictError",
    "Should not merge when a transaction is in progress"
  );

  info("Commit open transaction");
  await buf.db.execute("COMMIT");

  info("Merging should succeed after committing");
  await buf.apply();

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_abort_store() {
  let buf = await openMirror("abort_store");

  let controller = new AbortController();
  controller.abort();
  await Assert.rejects(
    storeRecords(
      buf,
      [
        {
          id: "menu",
          parentid: "places",
          type: "folder",
          children: [],
        },
      ],
      { signal: controller.signal }
    ),
    ex => ex.name == "InterruptedError",
    "Should abort storing when signaled"
  );

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_abort_merging() {
  let buf = await openMirror("abort_merging");

  let controller = new AbortController();
  controller.abort();
  await Assert.rejects(
    buf.apply({ signal: controller.signal }),
    ex => ex.name == "InterruptedError",
    "Should abort merge when signaled"
  );

  // Even though the merger is already finalized on the Rust side, the DB
  // connection is still open on the JS side. Finalizing `buf` closes it.
  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_blocker_state() {
  let barrier = new AsyncShutdown.Barrier("Test");
  let buf = await SyncedBookmarksMirror.open({
    path: "blocker_state_buf.sqlite",
    finalizeAt: barrier.client,
    recordStepTelemetry() {},
    recordValidationTelemetry() {},
  });
  await storeRecords(buf, [
    {
      id: "menu",
      parentid: "places",
      type: "folder",
      children: ["bookmarkAAAA"],
    },
    {
      id: "bookmarkAAAA",
      parentid: "menu",
      type: "bookmark",
      title: "A",
      bmkUri: "http://example.com/a",
    },
  ]);

  await buf.tryApply(buf.finalizeController.signal);
  await barrier.wait();

  let state = buf.progress.fetchState();
  let names = [];
  for (let s of state.steps) {
    equal(typeof s.at, "number", `Should report timestamp for ${s.step}`);
    switch (s.step) {
      case "fetchLocalTree":
        greaterOrEqual(
          s.took,
          0,
          "Should report time taken to fetch local tree"
        );
        deepEqual(
          s.counts,
          [
            { name: "items", count: 6 },
            { name: "deletions", count: 0 },
          ],
          "Should report number of items in local tree"
        );
        break;

      case "fetchRemoteTree":
        greaterOrEqual(
          s.took,
          0,
          "Should report time taken to fetch remote tree"
        );
        deepEqual(
          s.counts,
          [
            { name: "items", count: 6 },
            { name: "deletions", count: 0 },
          ],
          "Should report number of items in remote tree"
        );
        break;

      case "merge":
        greaterOrEqual(s.took, 0, "Should report time taken to merge");
        deepEqual(
          s.counts,
          [{ name: "items", count: 6 }],
          "Should report merge stats"
        );
        break;

      case "apply":
        greaterOrEqual(s.took, 0, "Should report time taken to apply");
        ok(!("counts" in s), "Should not report counts for applying");
        break;

      case "notifyObservers":
        greaterOrEqual(
          s.took,
          0,
          "Should report time taken to notify observers"
        );
        ok(!("counts" in s), "Should not report counts for observers");
        break;

      case "fetchLocalChangeRecords":
        greaterOrEqual(
          s.took,
          0,
          "Should report time taken to fetch records for upload"
        );
        deepEqual(
          s.counts,
          [{ name: "items", count: 4 }],
          "Should report number of records to upload"
        );
        break;

      case "finalize":
        ok(!("took" in s), "Should not report time taken to finalize");
        ok(!("counts" in s), "Should not report counts for finalizing");
    }
    names.push(s.step);
  }
  deepEqual(
    names,
    [
      "fetchLocalTree",
      "fetchRemoteTree",
      "merge",
      "apply",
      "notifyObservers",
      "fetchLocalChangeRecords",
      "finalize",
    ],
    "Should report merge progress after waiting on blocker"
  );
  ok(
    buf.finalizeController.signal.aborted,
    "Should abort finalize signal on shutdown"
  );

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});
