/* 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/>. */

// Test basic worker functionality: the main thread and worker can be
// separately controlled from the same debugger.

"use strict";

add_task(async function () {
  await pushPref("devtools.debugger.threads-visible", true);

  const dbg = await initDebugger(
    "doc-windowless-workers.html",
    "simple-worker.js"
  );
  const mainThread = dbg.toolbox.threadFront.actor;

  await waitForThreadCount(dbg, 2);
  const workers = dbg.selectors.getThreads();
  Assert.equal(workers.length, 2, "Got two workers");
  const thread1 = workers[0].actor;
  const thread2 = workers[1].actor;

  const mainThreadSource = findSource(dbg, "doc-windowless-workers.html");

  info("Pause in the main thread");
  assertNotPaused(dbg);
  await dbg.actions.breakOnNext();
  await waitForPaused(dbg, "doc-windowless-workers.html");
  await assertPausedAtSourceAndLine(dbg, mainThreadSource.id, 10);
  threadIsSelected(dbg, 1);

  info("Pause in the first worker");
  await dbg.actions.selectThread(thread1);
  assertNotPaused(dbg);
  await dbg.actions.breakOnNext();
  await waitForPaused(dbg, "simple-worker.js");
  threadIsSelected(dbg, 2);
  const workerSource2 = dbg.selectors.getSelectedSource();
  await assertPausedAtSourceAndLine(dbg, workerSource2.id, 3);

  info("Add a watch expression and view the value");
  await addExpression(dbg, "count");
  is(getWatchExpressionLabel(dbg, 1), "count");
  const v = getWatchExpressionValue(dbg, 1);
  Assert.equal(v, `${+v}`, "Value of count should be a number");

  info("StepOver in the first worker");
  await stepOver(dbg);
  await assertPausedAtSourceAndLine(dbg, workerSource2.id, 4);

  info("Ensure that the watch expression has updated");
  await waitUntil(() => {
    const v2 = getWatchExpressionValue(dbg, 1);
    return +v2 == +v + 1;
  });

  info("Resume in the first worker");
  await resume(dbg);
  assertNotPaused(dbg);

  info("StepOver in the main thread");
  dbg.actions.selectThread(mainThread);
  await stepOver(dbg);
  await assertPausedAtSourceAndLine(dbg, mainThreadSource.id, 11);

  info("Resume in the mainThread");
  await resume(dbg);
  assertNotPaused(dbg);

  info("Pause in both workers");
  await addBreakpoint(dbg, "simple-worker.js", 10);
  invokeInTab("sayHello");

  info("Wait for both workers to pause");
  // When a thread pauses the current thread changes,
  // and we don't want to get confused.
  await waitForPausedThread(dbg, thread1);
  await waitForPausedThread(dbg, thread2);
  threadIsPaused(dbg, 2);
  threadIsPaused(dbg, 3);

  info("View the first paused thread");
  dbg.actions.selectThread(thread1);
  await waitForPaused(dbg);
  await assertPausedAtSourceAndLine(dbg, workerSource2.id, 10);

  info("View the second paused thread");
  await dbg.actions.selectThread(thread2);
  threadIsSelected(dbg, 3);
  await waitForPaused(dbg);
  const workerSource3 = dbg.selectors.getSelectedSource();
  is(
    workerSource2,
    workerSource3,
    "The selected source is the same as we have one source per URL"
  );
  await assertPausedAtSourceAndLine(dbg, workerSource3.id, 10);

  info("StepOver in second worker and not the first");
  await stepOver(dbg);
  await assertPausedAtSourceAndLine(dbg, workerSource3.id, 11);
  await dbg.actions.selectThread(thread1);
  await assertPausedAtSourceAndLine(dbg, workerSource2.id, 10);

  info("Resume both worker execution");
  await resume(dbg);
  assertNotPaused(dbg);
  await dbg.actions.selectThread(thread2);
  await resume(dbg);
  assertNotPaused(dbg);

  let sourceActors = dbg.selectors.getSourceActorsForSource(workerSource3.id);
  is(
    sourceActors.length,
    2,
    "There is one source actor per thread for the worker source"
  );
  info(
    "Terminate the first worker and wait for having only the second worker in threads list"
  );
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.wrappedJSObject.worker1.terminate();
  });
  await waitForThreadCount(dbg, 1);
  sourceActors = dbg.selectors.getSourceActorsForSource(workerSource3.id);
  is(
    sourceActors.length,
    1,
    "After the first worker is destroyed, we only have one source actor for the worker source"
  );
  ok(
    sourceExists(dbg, "simple-worker.js"),
    "But we still have the worker source object"
  );

  info("Terminate the second worker and wait for no more additional threads");
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    content.wrappedJSObject.worker2.terminate();
  });
  await waitForThreadCount(dbg, 0);
  sourceActors = dbg.selectors.getSourceActorsForSource(workerSource3.id);
  is(
    sourceActors.length,
    0,
    "After all workers are destroyed, we no longer have any source actor"
  );
  ok(
    !sourceExists(dbg, "simple-worker.js"),
    "And we no longer have the worker source"
  );
});

function assertClass(dbg, selector, className, ...args) {
  ok(
    findElement(dbg, selector, ...args).classList.contains(className),
    `${className} class exists`
  );
}

function threadIsPaused(dbg, index) {
  return ok(
    findElement(dbg, "threadsPaneItemPause", index),
    `Thread ${index} is paused`
  );
}

function threadIsSelected(dbg, index) {
  return assertClass(dbg, "threadsPaneItem", "selected", index);
}
