/**
 * Default list of commands to execute for a PeerConnection test.
 */

const STABLE = "stable";
const HAVE_LOCAL_OFFER = "have-local-offer";
const HAVE_REMOTE_OFFER = "have-remote-offer";
const CLOSED = "closed";

const ICE_NEW = "new";
const GATH_NEW = "new";
const GATH_GATH = "gathering";
const GATH_COMPLETE = "complete";

function deltaSeconds(date1, date2) {
  return (date2.getTime() - date1.getTime()) / 1000;
}

function dumpSdp(test) {
  if (typeof test._local_offer !== "undefined") {
    dump("ERROR: SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, ""));
  }
  if (typeof test._remote_answer !== "undefined") {
    dump("ERROR: SDP answer: " + test._remote_answer.sdp.replace(/[\r]/g, ""));
  }

  if (
    test.pcLocal &&
    typeof test.pcLocal._local_ice_candidates !== "undefined"
  ) {
    dump(
      "pcLocal._local_ice_candidates: " +
        JSON.stringify(test.pcLocal._local_ice_candidates) +
        "\n"
    );
    dump(
      "pcLocal._remote_ice_candidates: " +
        JSON.stringify(test.pcLocal._remote_ice_candidates) +
        "\n"
    );
    dump(
      "pcLocal._ice_candidates_to_add: " +
        JSON.stringify(test.pcLocal._ice_candidates_to_add) +
        "\n"
    );
  }
  if (
    test.pcRemote &&
    typeof test.pcRemote._local_ice_candidates !== "undefined"
  ) {
    dump(
      "pcRemote._local_ice_candidates: " +
        JSON.stringify(test.pcRemote._local_ice_candidates) +
        "\n"
    );
    dump(
      "pcRemote._remote_ice_candidates: " +
        JSON.stringify(test.pcRemote._remote_ice_candidates) +
        "\n"
    );
    dump(
      "pcRemote._ice_candidates_to_add: " +
        JSON.stringify(test.pcRemote._ice_candidates_to_add) +
        "\n"
    );
  }

  if (test.pcLocal && typeof test.pcLocal.iceConnectionLog !== "undefined") {
    dump(
      "pcLocal ICE connection state log: " +
        test.pcLocal.iceConnectionLog +
        "\n"
    );
  }
  if (test.pcRemote && typeof test.pcRemote.iceConnectionLog !== "undefined") {
    dump(
      "pcRemote ICE connection state log: " +
        test.pcRemote.iceConnectionLog +
        "\n"
    );
  }

  if (
    test.pcLocal &&
    test.pcRemote &&
    typeof test.pcLocal.setRemoteDescDate !== "undefined" &&
    typeof test.pcRemote.setLocalDescDate !== "undefined"
  ) {
    var delta = deltaSeconds(
      test.pcLocal.setRemoteDescDate,
      test.pcRemote.setLocalDescDate
    );
    dump(
      "Delay between pcLocal.setRemote <-> pcRemote.setLocal: " + delta + "\n"
    );
  }
  if (
    test.pcLocal &&
    test.pcRemote &&
    typeof test.pcLocal.setRemoteDescDate !== "undefined" &&
    typeof test.pcLocal.setRemoteDescStableEventDate !== "undefined"
  ) {
    var delta = deltaSeconds(
      test.pcLocal.setRemoteDescDate,
      test.pcLocal.setRemoteDescStableEventDate
    );
    dump(
      "Delay between pcLocal.setRemote <-> pcLocal.signalingStateStable: " +
        delta +
        "\n"
    );
  }
  if (
    test.pcLocal &&
    test.pcRemote &&
    typeof test.pcRemote.setLocalDescDate !== "undefined" &&
    typeof test.pcRemote.setLocalDescStableEventDate !== "undefined"
  ) {
    var delta = deltaSeconds(
      test.pcRemote.setLocalDescDate,
      test.pcRemote.setLocalDescStableEventDate
    );
    dump(
      "Delay between pcRemote.setLocal <-> pcRemote.signalingStateStable: " +
        delta +
        "\n"
    );
  }
}

// We need to verify that at least one candidate has been (or will be) gathered.
function waitForAnIceCandidate(pc) {
  return new Promise(resolve => {
    if (!pc.localRequiresTrickleIce || pc._new_local_ice_candidates.length) {
      resolve();
    } else {
      // In some circumstances, especially when both PCs are on the same
      // browser, even though we are connected, the connection can be
      // established without receiving a single candidate from one or other
      // peer.  So we wait for at least one...
      pc._pc.addEventListener("icecandidate", resolve);
    }
  }).then(() => {
    ok(
      pc._local_ice_candidates.length,
      pc + " received local trickle ICE candidates"
    );
    isnot(
      pc._pc.iceGatheringState,
      GATH_NEW,
      pc + " ICE gathering state is not 'new'"
    );
  });
}

async function checkTrackStats(pc, track, outbound) {
  const audio = track.kind == "audio";
  const msg =
    `${pc} stats ${outbound ? "outbound " : "inbound "}` +
    `${audio ? "audio" : "video"} rtp track id ${track.id}`;
  const stats = await pc.getStats(track);
  ok(
    pc.hasStat(stats, {
      type: outbound ? "outbound-rtp" : "inbound-rtp",
      kind: audio ? "audio" : "video",
    }),
    `${msg} - found expected stats`
  );
  ok(
    !pc.hasStat(stats, {
      type: outbound ? "inbound-rtp" : "outbound-rtp",
    }),
    `${msg} - did not find extra stats with wrong direction`
  );
  ok(
    !pc.hasStat(stats, {
      kind: audio ? "video" : "audio",
    }),
    `${msg} - did not find extra stats with wrong media type`
  );
}

function checkAllTrackStats(pc) {
  return Promise.all([
    ...pc
      .getExpectedActiveReceivers()
      .map(({ track }) => checkTrackStats(pc, track, false)),
    ...pc
      .getExpectedSenders()
      .map(({ track }) => checkTrackStats(pc, track, true)),
  ]);
}

// Commands run once at the beginning of each test, even when performing a
// renegotiation test.
var commandsPeerConnectionInitial = [
  function PC_LOCAL_SETUP_ICE_LOGGER(test) {
    test.pcLocal.logIceConnectionState();
  },

  function PC_REMOTE_SETUP_ICE_LOGGER(test) {
    test.pcRemote.logIceConnectionState();
  },

  function PC_LOCAL_SETUP_SIGNALING_LOGGER(test) {
    test.pcLocal.logSignalingState();
  },

  function PC_REMOTE_SETUP_SIGNALING_LOGGER(test) {
    test.pcRemote.logSignalingState();
  },

  function PC_LOCAL_SETUP_TRACK_HANDLER(test) {
    test.pcLocal.setupTrackEventHandler();
  },

  function PC_REMOTE_SETUP_TRACK_HANDLER(test) {
    test.pcRemote.setupTrackEventHandler();
  },

  function PC_LOCAL_CHECK_INITIAL_SIGNALINGSTATE(test) {
    is(
      test.pcLocal.signalingState,
      STABLE,
      "Initial local signalingState is 'stable'"
    );
  },

  function PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE(test) {
    is(
      test.pcRemote.signalingState,
      STABLE,
      "Initial remote signalingState is 'stable'"
    );
  },

  function PC_LOCAL_CHECK_INITIAL_ICE_STATE(test) {
    is(
      test.pcLocal.iceConnectionState,
      ICE_NEW,
      "Initial local ICE connection state is 'new'"
    );
  },

  function PC_REMOTE_CHECK_INITIAL_ICE_STATE(test) {
    is(
      test.pcRemote.iceConnectionState,
      ICE_NEW,
      "Initial remote ICE connection state is 'new'"
    );
  },

  function PC_LOCAL_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
    is(
      test.pcLocal._pc.canTrickleIceCandidates,
      null,
      "Local trickle status should start out unknown"
    );
  },

  function PC_REMOTE_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
    is(
      test.pcRemote._pc.canTrickleIceCandidates,
      null,
      "Remote trickle status should start out unknown"
    );
  },
];

var commandsGetUserMedia = [
  function PC_LOCAL_GUM(test) {
    return test.pcLocal.getAllUserMediaAndAddStreams(test.pcLocal.constraints);
  },

  function PC_REMOTE_GUM(test) {
    return test.pcRemote.getAllUserMediaAndAddStreams(
      test.pcRemote.constraints
    );
  },
];

var commandsPeerConnectionOfferAnswer = [
  function PC_LOCAL_SETUP_ICE_HANDLER(test) {
    test.pcLocal.setupIceCandidateHandler(test);
  },

  function PC_REMOTE_SETUP_ICE_HANDLER(test) {
    test.pcRemote.setupIceCandidateHandler(test);
  },

  function PC_LOCAL_CREATE_OFFER(test) {
    return test.createOffer(test.pcLocal).then(offer => {
      is(
        test.pcLocal.signalingState,
        STABLE,
        "Local create offer does not change signaling state"
      );
    });
  },

  function PC_LOCAL_SET_LOCAL_DESCRIPTION(test) {
    return test
      .setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER)
      .then(() => {
        is(
          test.pcLocal.signalingState,
          HAVE_LOCAL_OFFER,
          "signalingState after local setLocalDescription is 'have-local-offer'"
        );
      });
  },

  function PC_REMOTE_GET_OFFER(test) {
    test._local_offer = test.originalOffer;
    test._offer_constraints = test.pcLocal.constraints;
    test._offer_options = test.pcLocal.offerOptions;
    return Promise.resolve();
  },

  function PC_REMOTE_SET_REMOTE_DESCRIPTION(test) {
    return test
      .setRemoteDescription(test.pcRemote, test._local_offer, HAVE_REMOTE_OFFER)
      .then(() => {
        is(
          test.pcRemote.signalingState,
          HAVE_REMOTE_OFFER,
          "signalingState after remote setRemoteDescription is 'have-remote-offer'"
        );
      });
  },

  function PC_REMOTE_CHECK_CAN_TRICKLE_SYNC(test) {
    is(
      test.pcRemote._pc.canTrickleIceCandidates,
      true,
      "Remote thinks that local can trickle"
    );
  },

  function PC_LOCAL_SANE_LOCAL_SDP(test) {
    test.pcLocal.localRequiresTrickleIce = sdputils.verifySdp(
      test._local_offer,
      "offer",
      test._offer_constraints,
      test._offer_options,
      test.testOptions
    );
  },

  function PC_REMOTE_SANE_REMOTE_SDP(test) {
    test.pcRemote.remoteRequiresTrickleIce = sdputils.verifySdp(
      test._local_offer,
      "offer",
      test._offer_constraints,
      test._offer_options,
      test.testOptions
    );
  },

  function PC_REMOTE_CREATE_ANSWER(test) {
    return test.createAnswer(test.pcRemote).then(answer => {
      is(
        test.pcRemote.signalingState,
        HAVE_REMOTE_OFFER,
        "Remote createAnswer does not change signaling state"
      );
    });
  },

  function PC_REMOTE_SET_LOCAL_DESCRIPTION(test) {
    return test
      .setLocalDescription(test.pcRemote, test.originalAnswer, STABLE)
      .then(() => {
        is(
          test.pcRemote.signalingState,
          STABLE,
          "signalingState after remote setLocalDescription is 'stable'"
        );
      });
  },

  function PC_LOCAL_GET_ANSWER(test) {
    test._remote_answer = test.originalAnswer;
    test._answer_constraints = test.pcRemote.constraints;
    return Promise.resolve();
  },

  function PC_LOCAL_SET_REMOTE_DESCRIPTION(test) {
    return test
      .setRemoteDescription(test.pcLocal, test._remote_answer, STABLE)
      .then(() => {
        is(
          test.pcLocal.signalingState,
          STABLE,
          "signalingState after local setRemoteDescription is 'stable'"
        );
      });
  },

  function PC_REMOTE_SANE_LOCAL_SDP(test) {
    test.pcRemote.localRequiresTrickleIce = sdputils.verifySdp(
      test._remote_answer,
      "answer",
      test._offer_constraints,
      test._offer_options,
      test.testOptions
    );
  },
  function PC_LOCAL_SANE_REMOTE_SDP(test) {
    test.pcLocal.remoteRequiresTrickleIce = sdputils.verifySdp(
      test._remote_answer,
      "answer",
      test._offer_constraints,
      test._offer_options,
      test.testOptions
    );
  },

  function PC_LOCAL_CHECK_CAN_TRICKLE_SYNC(test) {
    is(
      test.pcLocal._pc.canTrickleIceCandidates,
      true,
      "Local thinks that remote can trickle"
    );
  },

  function PC_LOCAL_WAIT_FOR_ICE_CONNECTED(test) {
    return test.pcLocal.waitForIceConnected().then(() => {
      info(
        test.pcLocal +
          ": ICE connection state log: " +
          test.pcLocal.iceConnectionLog
      );
    });
  },

  function PC_REMOTE_WAIT_FOR_ICE_CONNECTED(test) {
    return test.pcRemote.waitForIceConnected().then(() => {
      info(
        test.pcRemote +
          ": ICE connection state log: " +
          test.pcRemote.iceConnectionLog
      );
    });
  },

  function PC_LOCAL_VERIFY_ICE_GATHERING(test) {
    return waitForAnIceCandidate(test.pcLocal);
  },

  function PC_REMOTE_VERIFY_ICE_GATHERING(test) {
    return waitForAnIceCandidate(test.pcRemote);
  },

  function PC_LOCAL_WAIT_FOR_MEDIA_FLOW(test) {
    return test.pcLocal.waitForMediaFlow();
  },

  function PC_REMOTE_WAIT_FOR_MEDIA_FLOW(test) {
    return test.pcRemote.waitForMediaFlow();
  },

  function PC_LOCAL_CHECK_STATS(test) {
    return test.pcLocal.getStats().then(stats => {
      test.pcLocal.checkStats(stats);
    });
  },

  function PC_REMOTE_CHECK_STATS(test) {
    return test.pcRemote.getStats().then(stats => {
      test.pcRemote.checkStats(stats);
    });
  },

  function PC_LOCAL_CHECK_ICE_CONNECTION_TYPE(test) {
    return test.pcLocal.getStats().then(stats => {
      test.pcLocal.checkStatsIceConnectionType(
        stats,
        test.testOptions.expectedLocalCandidateType
      );
    });
  },

  function PC_REMOTE_CHECK_ICE_CONNECTION_TYPE(test) {
    return test.pcRemote.getStats().then(stats => {
      test.pcRemote.checkStatsIceConnectionType(
        stats,
        test.testOptions.expectedRemoteCandidateType
      );
    });
  },

  function PC_LOCAL_CHECK_ICE_CONNECTIONS(test) {
    return test.pcLocal.getStats().then(stats => {
      test.pcLocal.checkStatsIceConnections(stats, test.testOptions);
    });
  },

  function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) {
    return test.pcRemote.getStats().then(stats => {
      test.pcRemote.checkStatsIceConnections(stats, test.testOptions);
    });
  },

  function PC_LOCAL_CHECK_MSID(test) {
    return test.pcLocal.checkLocalMsids();
  },
  function PC_REMOTE_CHECK_MSID(test) {
    return test.pcRemote.checkLocalMsids();
  },

  function PC_LOCAL_CHECK_TRACK_STATS(test) {
    return checkAllTrackStats(test.pcLocal);
  },
  function PC_REMOTE_CHECK_TRACK_STATS(test) {
    return checkAllTrackStats(test.pcRemote);
  },
  function PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) {
    if (test.pcLocal.endOfTrickleSdp) {
      /* In case the endOfTrickleSdp promise is resolved already it will win the
       * race because it gets evaluated first. But if endOfTrickleSdp is still
       * pending the rejection will win the race. */
      return Promise.race([
        test.pcLocal.endOfTrickleSdp,
        Promise.reject("No SDP"),
      ]).then(
        sdp =>
          sdputils.checkSdpAfterEndOfTrickle(
            sdp,
            test.testOptions,
            test.pcLocal.label
          ),
        () =>
          info(
            "pcLocal: Gathering is not complete yet, skipping post-gathering SDP check"
          )
      );
    }
  },
  function PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) {
    if (test.pcRemote.endOfTrickleSdp) {
      /* In case the endOfTrickleSdp promise is resolved already it will win the
       * race because it gets evaluated first. But if endOfTrickleSdp is still
       * pending the rejection will win the race. */
      return Promise.race([
        test.pcRemote.endOfTrickleSdp,
        Promise.reject("No SDP"),
      ]).then(
        sdp =>
          sdputils.checkSdpAfterEndOfTrickle(
            sdp,
            test.testOptions,
            test.pcRemote.label
          ),
        () =>
          info(
            "pcRemote: Gathering is not complete yet, skipping post-gathering SDP check"
          )
      );
    }
  },
];

function PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER(test) {
  isnot(
    test.originalOffer.sdp.search("H264/90000"),
    -1,
    "H.264 should be present in the SDP offer"
  );
  test.originalOffer.sdp = sdputils.removeCodecs(
    test.originalOffer.sdp,
    [120, 121, 97]
  );
  info("Updated H264 only offer: " + JSON.stringify(test.originalOffer));
}

function PC_LOCAL_REMOVE_ALL_BUT_AV1_FROM_OFFER(test) {
  isnot(
    test.originalOffer.sdp.search("AV1/90000"),
    -1,
    "AV1 should be present in the SDP offer"
  );
  for (const codec of [103, 105, 120, 121, 126, 97]) {
    test.originalOffer.sdp = sdputils.removeCodec(
      test.originalOffer.sdp,
      codec
    );
  }
  info("Updated AV1 only offer: " + JSON.stringify(test.originalOffer));
}

function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) {
  test.originalOffer.sdp = sdputils.removeBundle(test.originalOffer.sdp);
  info("Updated no bundle offer: " + JSON.stringify(test.originalOffer));
}

function PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER(test) {
  test.originalOffer.sdp = sdputils.removeRtcpMux(test.originalOffer.sdp);
  info("Updated no RTCP-Mux offer: " + JSON.stringify(test.originalOffer));
}

function PC_LOCAL_REMOVE_SSRC_FROM_OFFER(test) {
  test.originalOffer.sdp = sdputils.removeSSRCs(test.originalOffer.sdp);
  info("Updated no SSRCs offer: " + JSON.stringify(test.originalOffer));
}

function PC_REMOTE_REMOVE_SSRC_FROM_ANSWER(test) {
  test.originalAnswer.sdp = sdputils.removeSSRCs(test.originalAnswer.sdp);
  info("Updated no SSRCs answer: " + JSON.stringify(test.originalAnswer));
}

var addRenegotiation = (chain, commands, checks) => {
  chain.append(commands);
  chain.append(commandsPeerConnectionOfferAnswer);
  if (checks) {
    chain.append(checks);
  }
};

var addRenegotiationAnswerer = (chain, commands, checks) => {
  chain.append(function SWAP_PC_LOCAL_PC_REMOTE(test) {
    var temp = test.pcLocal;
    test.pcLocal = test.pcRemote;
    test.pcRemote = temp;
  });
  addRenegotiation(chain, commands, checks);
};
