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

const ORIGIN = "https://example.com";

async function tryPeerConnection(browser, expectedError = null) {
  let errtype = await SpecialPowers.spawn(browser, [], async function () {
    let pc = new content.RTCPeerConnection();
    try {
      await pc.createOffer({ offerToReceiveAudio: true });
      return null;
    } catch (err) {
      return err.name;
    }
  });

  let detail = expectedError
    ? `createOffer() threw a ${expectedError}`
    : "createOffer() succeeded";
  is(errtype, expectedError, detail);
}

// Helper for tests that use the peer-request-allowed and -blocked events.
// A test that expects some of those events does the following:
//  - call Events.on() before the test to setup event handlers
//  - call Events.expect(name) after a specific event is expected to have
//    occured.  This will fail if the event didn't occur, and will return
//    the details passed to the handler for furhter checking.
//  - call Events.off() at the end of the test to clean up.  At this point, if
//    any events were triggered that the test did not expect, the test fails.
const Events = {
  events: ["peer-request-allowed", "peer-request-blocked"],
  details: new Map(),
  handlers: new Map(),
  on() {
    for (let event of this.events) {
      let handler = data => {
        if (this.details.has(event)) {
          ok(false, `Got multiple ${event} events`);
        }
        this.details.set(event, data);
      };
      webrtcUI.on(event, handler);
      this.handlers.set(event, handler);
    }
  },
  expect(event) {
    let result = this.details.get(event);
    isnot(result, undefined, `${event} event was triggered`);
    this.details.delete(event);

    // All events should have a good origin
    is(result.origin, ORIGIN, `${event} event has correct origin`);

    return result;
  },
  off() {
    for (let event of this.events) {
      webrtcUI.off(event, this.handlers.get(event));
      this.handlers.delete(event);
    }
    for (let [event] of this.details) {
      ok(false, `Got unexpected event ${event}`);
    }
  },
};

var gTests = [
  {
    desc: "Basic peer-request-allowed event",
    run: async function testPeerRequestEvent(browser) {
      Events.on();

      await tryPeerConnection(browser);

      let details = Events.expect("peer-request-allowed");
      isnot(
        details.callID,
        undefined,
        "peer-request-allowed event includes callID"
      );
      isnot(
        details.windowID,
        undefined,
        "peer-request-allowed event includes windowID"
      );

      Events.off();
    },
  },

  {
    desc: "Immediate peer connection blocker can allow",
    run: async function testBlocker(browser) {
      Events.on();

      let blockerCalled = false;
      let blocker = params => {
        is(
          params.origin,
          ORIGIN,
          "Peer connection blocker origin parameter is correct"
        );
        blockerCalled = true;
        return "allow";
      };

      webrtcUI.addPeerConnectionBlocker(blocker);

      await tryPeerConnection(browser);
      is(blockerCalled, true, "Blocker was called");
      Events.expect("peer-request-allowed");

      webrtcUI.removePeerConnectionBlocker(blocker);
      Events.off();
    },
  },

  {
    desc: "Deferred peer connection blocker can allow",
    run: async function testDeferredBlocker(browser) {
      Events.on();

      let blocker = params => Promise.resolve("allow");
      webrtcUI.addPeerConnectionBlocker(blocker);

      await tryPeerConnection(browser);
      Events.expect("peer-request-allowed");

      webrtcUI.removePeerConnectionBlocker(blocker);
      Events.off();
    },
  },

  {
    desc: "Immediate peer connection blocker can deny",
    run: async function testBlockerDeny(browser) {
      Events.on();

      let blocker = params => "deny";
      webrtcUI.addPeerConnectionBlocker(blocker);

      await tryPeerConnection(browser, "NotAllowedError");

      Events.expect("peer-request-blocked");

      webrtcUI.removePeerConnectionBlocker(blocker);
      Events.off();
    },
  },

  {
    desc: "Multiple blockers work (both allow)",
    run: async function testMultipleAllowBlockers(browser) {
      Events.on();

      let blocker1Called = false,
        blocker1 = params => {
          blocker1Called = true;
          return "allow";
        };
      webrtcUI.addPeerConnectionBlocker(blocker1);

      let blocker2Called = false,
        blocker2 = params => {
          blocker2Called = true;
          return "allow";
        };
      webrtcUI.addPeerConnectionBlocker(blocker2);

      await tryPeerConnection(browser);

      Events.expect("peer-request-allowed");
      ok(blocker1Called, "First blocker was called");
      ok(blocker2Called, "Second blocker was called");

      webrtcUI.removePeerConnectionBlocker(blocker1);
      webrtcUI.removePeerConnectionBlocker(blocker2);
      Events.off();
    },
  },

  {
    desc: "Multiple blockers work (allow then deny)",
    run: async function testAllowDenyBlockers(browser) {
      Events.on();

      let blocker1Called = false,
        blocker1 = params => {
          blocker1Called = true;
          return "allow";
        };
      webrtcUI.addPeerConnectionBlocker(blocker1);

      let blocker2Called = false,
        blocker2 = params => {
          blocker2Called = true;
          return "deny";
        };
      webrtcUI.addPeerConnectionBlocker(blocker2);

      await tryPeerConnection(browser, "NotAllowedError");

      Events.expect("peer-request-blocked");
      ok(blocker1Called, "First blocker was called");
      ok(blocker2Called, "Second blocker was called");

      webrtcUI.removePeerConnectionBlocker(blocker1);
      webrtcUI.removePeerConnectionBlocker(blocker2);
      Events.off();
    },
  },

  {
    desc: "Multiple blockers work (deny first)",
    run: async function testDenyAllowBlockers(browser) {
      Events.on();

      let blocker1Called = false,
        blocker1 = params => {
          blocker1Called = true;
          return "deny";
        };
      webrtcUI.addPeerConnectionBlocker(blocker1);

      let blocker2Called = false,
        blocker2 = params => {
          blocker2Called = true;
          return "allow";
        };
      webrtcUI.addPeerConnectionBlocker(blocker2);

      await tryPeerConnection(browser, "NotAllowedError");

      Events.expect("peer-request-blocked");
      ok(blocker1Called, "First blocker was called");
      ok(
        !blocker2Called,
        "Peer connection blocker after a deny is not invoked"
      );

      webrtcUI.removePeerConnectionBlocker(blocker1);
      webrtcUI.removePeerConnectionBlocker(blocker2);
      Events.off();
    },
  },

  {
    desc: "Blockers may be removed",
    run: async function testRemoveBlocker(browser) {
      Events.on();

      let blocker1Called = false,
        blocker1 = params => {
          blocker1Called = true;
          return "allow";
        };
      webrtcUI.addPeerConnectionBlocker(blocker1);

      let blocker2Called = false,
        blocker2 = params => {
          blocker2Called = true;
          return "allow";
        };
      webrtcUI.addPeerConnectionBlocker(blocker2);
      webrtcUI.removePeerConnectionBlocker(blocker1);

      await tryPeerConnection(browser);

      Events.expect("peer-request-allowed");

      ok(!blocker1Called, "Removed peer connection blocker is not invoked");
      ok(blocker2Called, "Second peer connection blocker was invoked");

      webrtcUI.removePeerConnectionBlocker(blocker2);
      Events.off();
    },
  },

  {
    desc: "Blocker that throws is ignored",
    run: async function testBlockerThrows(browser) {
      Events.on();
      let blocker1Called = false,
        blocker1 = params => {
          blocker1Called = true;
          throw new Error("kaboom");
        };
      webrtcUI.addPeerConnectionBlocker(blocker1);

      let blocker2Called = false,
        blocker2 = params => {
          blocker2Called = true;
          return "allow";
        };
      webrtcUI.addPeerConnectionBlocker(blocker2);

      await tryPeerConnection(browser);

      Events.expect("peer-request-allowed");
      ok(blocker1Called, "First blocker was invoked");
      ok(blocker2Called, "Second blocker was invoked");

      webrtcUI.removePeerConnectionBlocker(blocker1);
      webrtcUI.removePeerConnectionBlocker(blocker2);
      Events.off();
    },
  },

  {
    desc: "Cancel peer request",
    run: async function testBlockerCancel(browser) {
      let blocker,
        blockerPromise = new Promise(resolve => {
          blocker = params => {
            resolve();
            // defer indefinitely
            return new Promise(innerResolve => {});
          };
        });
      webrtcUI.addPeerConnectionBlocker(blocker);

      await SpecialPowers.spawn(browser, [], async function () {
        new content.RTCPeerConnection().createOffer({
          offerToReceiveAudio: true,
        });
      });

      await blockerPromise;

      let eventPromise = new Promise(resolve => {
        webrtcUI.on("peer-request-cancel", function listener(details) {
          resolve(details);
          webrtcUI.off("peer-request-cancel", listener);
        });
      });

      await SpecialPowers.spawn(browser, [], async function () {
        content.location.reload();
      });

      let details = await eventPromise;
      isnot(
        details.callID,
        undefined,
        "peer-request-cancel event includes callID"
      );
      is(
        details.origin,
        ORIGIN,
        "peer-request-cancel event has correct origin"
      );

      webrtcUI.removePeerConnectionBlocker(blocker);
    },
  },
];

add_task(async function test() {
  await runTests(gTests, {
    skipObserverVerification: true,
    cleanup() {
      is(
        webrtcUI.peerConnectionBlockers.size,
        0,
        "Peer connection blockers list is empty"
      );
    },
  });
});
