/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */

#include <iostream>
#include <map>

#include "nspr.h"
#include "nss.h"
#include "ssl.h"

#include "mozilla/RefPtr.h"
#include "mozilla/Tuple.h"

#define GTEST_HAS_RTTI 0
#include "gtest/gtest.h"
#include "gtest_utils.h"

#include "FakeMediaStreams.h"
#include "FakeMediaStreamsImpl.h"
#include "FakeLogging.h"

#include "signaling/src/sdp/SdpMediaSection.h"
#include "signaling/src/sdp/SipccSdpParser.h"
#include "signaling/src/jsep/JsepCodecDescription.h"
#include "signaling/src/jsep/JsepTrack.h"
#include "signaling/src/jsep/JsepSession.h"
#include "signaling/src/jsep/JsepSessionImpl.h"
#include "signaling/src/jsep/JsepTrack.h"

#include "mtransport_test_utils.h"

#include "FakeIPC.h"
#include "FakeIPC.cpp"

#include "TestHarness.h"

namespace mozilla {
static std::string kAEqualsCandidate("a=candidate:");
const static size_t kNumCandidatesPerComponent = 3;

class JsepSessionTestBase : public ::testing::Test
{
};

class FakeUuidGenerator : public mozilla::JsepUuidGenerator
{
public:
  bool
  Generate(std::string* str)
  {
    std::ostringstream os;
    os << "FAKE_UUID_" << ++ctr;
    *str = os.str();

    return true;
  }

private:
  static uint64_t ctr;
};

uint64_t FakeUuidGenerator::ctr = 1000;

class JsepSessionTest : public JsepSessionTestBase,
                        public ::testing::WithParamInterface<std::string>
{
public:
  JsepSessionTest()
      : mSessionOff("Offerer", MakeUnique<FakeUuidGenerator>()),
        mSessionAns("Answerer", MakeUnique<FakeUuidGenerator>())
  {
    EXPECT_EQ(NS_OK, mSessionOff.Init());
    EXPECT_EQ(NS_OK, mSessionAns.Init());

    AddTransportData(&mSessionOff, &mOffererTransport);
    AddTransportData(&mSessionAns, &mAnswererTransport);
  }

protected:
  struct TransportData {
    std::string mIceUfrag;
    std::string mIcePwd;
    std::map<std::string, std::vector<uint8_t> > mFingerprints;
  };

  void
  AddDtlsFingerprint(const std::string& alg, JsepSessionImpl* session,
                     TransportData* tdata)
  {
    std::vector<uint8_t> fp;
    fp.assign((alg == "sha-1") ? 20 : 32,
              (session->GetName() == "Offerer") ? 0x4f : 0x41);
    session->AddDtlsFingerprint(alg, fp);
    tdata->mFingerprints[alg] = fp;
  }

  void
  AddTransportData(JsepSessionImpl* session, TransportData* tdata)
  {
    // Values here semi-borrowed from JSEP draft.
    tdata->mIceUfrag = session->GetName() + "-ufrag";
    tdata->mIcePwd = session->GetName() + "-1234567890";
    session->SetIceCredentials(tdata->mIceUfrag, tdata->mIcePwd);
    AddDtlsFingerprint("sha-1", session, tdata);
    AddDtlsFingerprint("sha-256", session, tdata);
  }

  std::string
  CreateOffer(const Maybe<JsepOfferOptions> options = Nothing())
  {
    JsepOfferOptions defaultOptions;
    const JsepOfferOptions& optionsRef = options ? *options : defaultOptions;
    std::string offer;
    nsresult rv = mSessionOff.CreateOffer(optionsRef, &offer);
    EXPECT_EQ(NS_OK, rv) << mSessionOff.GetLastError();

    std::cerr << "OFFER: " << offer << std::endl;

    ValidateTransport(mOffererTransport, offer);

    return offer;
  }

  void
  AddTracks(JsepSessionImpl& side)
  {
    // Add tracks.
    if (types.empty()) {
      types = BuildTypes(GetParam());
    }
    AddTracks(side, types);

    // Now that we have added streams, we expect audio, then video, then
    // application in the SDP, regardless of the order in which the streams were
    // added.
    std::sort(types.begin(), types.end());
  }

  void
  AddTracks(JsepSessionImpl& side, const std::string& mediatypes)
  {
    AddTracks(side, BuildTypes(mediatypes));
  }

  std::vector<SdpMediaSection::MediaType>
  BuildTypes(const std::string& mediatypes)
  {
    std::vector<SdpMediaSection::MediaType> result;
    size_t ptr = 0;

    for (;;) {
      size_t comma = mediatypes.find(',', ptr);
      std::string chunk = mediatypes.substr(ptr, comma - ptr);

      SdpMediaSection::MediaType type;
      if (chunk == "audio") {
        type = SdpMediaSection::kAudio;
      } else if (chunk == "video") {
        type = SdpMediaSection::kVideo;
      } else if (chunk == "datachannel") {
        type = SdpMediaSection::kApplication;
      } else {
        MOZ_CRASH();
      }
      result.push_back(type);

      if (comma == std::string::npos)
        break;
      ptr = comma + 1;
    }

    return result;
  }

  void
  AddTracks(JsepSessionImpl& side,
            const std::vector<SdpMediaSection::MediaType>& mediatypes)
  {
    FakeUuidGenerator uuid_gen;
    std::string stream_id;
    std::string track_id;

    ASSERT_TRUE(uuid_gen.Generate(&stream_id));

    AddTracksToStream(side, stream_id, mediatypes);
  }

  void
  AddTracksToStream(JsepSessionImpl& side,
                    const std::string stream_id,
                    const std::string& mediatypes)
  {
    AddTracksToStream(side, stream_id, BuildTypes(mediatypes));
  }

  void
  AddTracksToStream(JsepSessionImpl& side,
                    const std::string stream_id,
                    const std::vector<SdpMediaSection::MediaType>& mediatypes)

  {
    FakeUuidGenerator uuid_gen;
    std::string track_id;

    for (auto track = mediatypes.begin(); track != mediatypes.end(); ++track) {
      ASSERT_TRUE(uuid_gen.Generate(&track_id));

      RefPtr<JsepTrack> mst(new JsepTrack(*track, stream_id, track_id));
      side.AddTrack(mst);
    }
  }

  bool HasMediaStream(std::vector<RefPtr<JsepTrack>> tracks) const {
    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
      if ((*i)->GetMediaType() != SdpMediaSection::kApplication) {
        return 1;
      }
    }
    return 0;
  }

  const std::string GetFirstLocalStreamId(JsepSessionImpl& side) const {
    auto tracks = side.GetLocalTracks();
    return (*tracks.begin())->GetStreamId();
  }

  std::vector<std::string>
  GetMediaStreamIds(std::vector<RefPtr<JsepTrack>> tracks) const {
    std::vector<std::string> ids;
    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
      // data channels don't have msid's
      if ((*i)->GetMediaType() == SdpMediaSection::kApplication) {
        continue;
      }
      ids.push_back((*i)->GetStreamId());
    }
    return ids;
  }

  std::vector<std::string>
  GetLocalMediaStreamIds(JsepSessionImpl& side) const {
    return GetMediaStreamIds(side.GetLocalTracks());
  }

  std::vector<std::string>
  GetRemoteMediaStreamIds(JsepSessionImpl& side) const {
    return GetMediaStreamIds(side.GetRemoteTracks());
  }

  std::vector<std::string>
  sortUniqueStrVector(std::vector<std::string> in) const {
    std::sort(in.begin(), in.end());
    auto it = std::unique(in.begin(), in.end());
    in.resize( std::distance(in.begin(), it));
    return in;
  }

  std::vector<std::string>
  GetLocalUniqueStreamIds(JsepSessionImpl& side) const {
    return sortUniqueStrVector(GetLocalMediaStreamIds(side));
  }

  std::vector<std::string>
  GetRemoteUniqueStreamIds(JsepSessionImpl& side) const {
    return sortUniqueStrVector(GetRemoteMediaStreamIds(side));
  }

  RefPtr<JsepTrack> GetTrack(JsepSessionImpl& side,
                             SdpMediaSection::MediaType type,
                             size_t index) const {
    auto tracks = side.GetLocalTracks();

    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
      if ((*i)->GetMediaType() != type) {
        continue;
      }

      if (index != 0) {
        --index;
        continue;
      }

      return *i;
    }

    return RefPtr<JsepTrack>(nullptr);
  }

  RefPtr<JsepTrack> GetTrackOff(size_t index,
                                SdpMediaSection::MediaType type) {
    return GetTrack(mSessionOff, type, index);
  }

  RefPtr<JsepTrack> GetTrackAns(size_t index,
                                SdpMediaSection::MediaType type) {
    return GetTrack(mSessionAns, type, index);
  }

  class ComparePairsByLevel {
    public:
      bool operator()(const JsepTrackPair& lhs,
                      const JsepTrackPair& rhs) const {
        return lhs.mLevel < rhs.mLevel;
      }
  };

  std::vector<JsepTrackPair> GetTrackPairsByLevel(JsepSessionImpl& side) const {
    auto pairs = side.GetNegotiatedTrackPairs();
    std::sort(pairs.begin(), pairs.end(), ComparePairsByLevel());
    return pairs;
  }

  bool Equals(const SdpFingerprintAttributeList::Fingerprint& f1,
              const SdpFingerprintAttributeList::Fingerprint& f2) const {
    if (f1.hashFunc != f2.hashFunc) {
      return false;
    }

    if (f1.fingerprint != f2.fingerprint) {
      return false;
    }

    return true;
  }

  bool Equals(const SdpFingerprintAttributeList& f1,
              const SdpFingerprintAttributeList& f2) const {
    if (f1.mFingerprints.size() != f2.mFingerprints.size()) {
      return false;
    }

    for (size_t i=0; i<f1.mFingerprints.size(); ++i) {
      if (!Equals(f1.mFingerprints[i], f2.mFingerprints[i])) {
        return false;
      }
    }

    return true;
  }

  bool Equals(const UniquePtr<JsepDtlsTransport>& t1,
              const UniquePtr<JsepDtlsTransport>& t2) const {
    if (!t1 && !t2) {
      return true;
    }

    if (!t1 || !t2) {
      return false;
    }

    if (!Equals(t1->GetFingerprints(),  t2->GetFingerprints())) {
      return false;
    }

    if (t1->GetRole() != t2->GetRole()) {
      return false;
    }

    return true;
  }


  bool Equals(const UniquePtr<JsepIceTransport>& t1,
              const UniquePtr<JsepIceTransport>& t2) const {
    if (!t1 && !t2) {
      return true;
    }

    if (!t1 || !t2) {
      return false;
    }

    if (t1->GetUfrag() != t2->GetUfrag()) {
      return false;
    }

    if (t1->GetPassword() != t2->GetPassword()) {
      return false;
    }

    return true;
  }

  bool Equals(const RefPtr<JsepTransport>& t1,
              const RefPtr<JsepTransport>& t2) const {
    if (!t1 && !t2) {
      return true;
    }

    if (!t1 || !t2) {
      return false;
    }

    if (t1->mTransportId != t2->mTransportId) {
      return false;
    }

    if (t1->mComponents != t2->mComponents) {
      return false;
    }

    if (!Equals(t1->mIce, t2->mIce)) {
      return false;
    }

    return true;
  }

  bool Equals(const JsepTrackPair& p1,
              const JsepTrackPair& p2) const {
    if (p1.mLevel != p2.mLevel) {
      return false;
    }

    // We don't check things like mBundleLevel, since that can change without
    // any changes to the transport, which is what we're really interested in.

    if (p1.mSending.get() != p2.mSending.get()) {
      return false;
    }

    if (p1.mReceiving.get() != p2.mReceiving.get()) {
      return false;
    }

    if (!Equals(p1.mRtpTransport, p2.mRtpTransport)) {
      return false;
    }

    if (!Equals(p1.mRtcpTransport, p2.mRtcpTransport)) {
      return false;
    }

    return true;
  }

  size_t GetTrackCount(JsepSessionImpl& side,
                       SdpMediaSection::MediaType type) const {
    auto tracks = side.GetLocalTracks();
    size_t result = 0;
    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
      if ((*i)->GetMediaType() == type) {
        ++result;
      }
    }
    return result;
  }

  UniquePtr<Sdp> GetParsedLocalDescription(const JsepSessionImpl& side) const {
    return Parse(side.GetLocalDescription());
  }

  SdpMediaSection* GetMsection(Sdp& sdp,
                               SdpMediaSection::MediaType type,
                               size_t index) const {
    for (size_t i = 0; i < sdp.GetMediaSectionCount(); ++i) {
      auto& msection = sdp.GetMediaSection(i);
      if (msection.GetMediaType() != type) {
        continue;
      }

      if (index) {
        --index;
        continue;
      }

      return &msection;
    }

    return nullptr;
  }

  void
  SetPayloadTypeNumber(JsepSession& session,
                       const std::string& codecName,
                       const std::string& payloadType)
  {
    for (auto* codec : session.Codecs()) {
      if (codec->mName == codecName) {
        codec->mDefaultPt = payloadType;
      }
    }
  }

  void
  SetCodecEnabled(JsepSession& session,
                  const std::string& codecName,
                  bool enabled)
  {
    for (auto* codec : session.Codecs()) {
      if (codec->mName == codecName) {
        codec->mEnabled = enabled;
      }
    }
  }

  void
  EnsureNegotiationFailure(SdpMediaSection::MediaType type,
                           const std::string& codecName)
  {
    for (auto i = mSessionOff.Codecs().begin(); i != mSessionOff.Codecs().end();
         ++i) {
      auto* codec = *i;
      if (codec->mType == type && codec->mName != codecName) {
        codec->mEnabled = false;
      }
    }

    for (auto i = mSessionAns.Codecs().begin(); i != mSessionAns.Codecs().end();
         ++i) {
      auto* codec = *i;
      if (codec->mType == type && codec->mName == codecName) {
        codec->mEnabled = false;
      }
    }
  }

  std::string
  CreateAnswer()
  {
    JsepAnswerOptions options;
    std::string answer;
    nsresult rv = mSessionAns.CreateAnswer(options, &answer);
    EXPECT_EQ(NS_OK, rv);

    std::cerr << "ANSWER: " << answer << std::endl;

    ValidateTransport(mAnswererTransport, answer);

    return answer;
  }

  static const uint32_t NO_CHECKS = 0;
  static const uint32_t CHECK_SUCCESS = 1;
  static const uint32_t CHECK_TRACKS = 1 << 2;
  static const uint32_t ALL_CHECKS = CHECK_SUCCESS | CHECK_TRACKS;

  void OfferAnswer(uint32_t checkFlags = ALL_CHECKS,
                   const Maybe<JsepOfferOptions> options = Nothing()) {
    std::string offer = CreateOffer(options);
    SetLocalOffer(offer, checkFlags);
    SetRemoteOffer(offer, checkFlags);

    std::string answer = CreateAnswer();
    SetLocalAnswer(answer, checkFlags);
    SetRemoteAnswer(answer, checkFlags);
  }

  void
  SetLocalOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
  {
    nsresult rv = mSessionOff.SetLocalDescription(kJsepSdpOffer, offer);

    if (checkFlags & CHECK_SUCCESS) {
      ASSERT_EQ(NS_OK, rv);
    }

    if (checkFlags & CHECK_TRACKS) {
      // Check that the transports exist.
      ASSERT_EQ(types.size(), mSessionOff.GetTransports().size());
      auto tracks = mSessionOff.GetLocalTracks();
      for (size_t i = 0; i < types.size(); ++i) {
        ASSERT_NE("", tracks[i]->GetStreamId());
        ASSERT_NE("", tracks[i]->GetTrackId());
        if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) {
          std::string msidAttr("a=msid:");
          msidAttr += tracks[i]->GetStreamId();
          msidAttr += " ";
          msidAttr += tracks[i]->GetTrackId();
          ASSERT_NE(std::string::npos, offer.find(msidAttr))
            << "Did not find " << msidAttr << " in offer";
        }
      }
    }
  }

  void
  SetRemoteOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
  {
    nsresult rv = mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer);

    if (checkFlags & CHECK_SUCCESS) {
      ASSERT_EQ(NS_OK, rv);
    }

    if (checkFlags & CHECK_TRACKS) {
      auto tracks = mSessionAns.GetRemoteTracks();
      // Now verify that the right stuff is in the tracks.
      ASSERT_EQ(types.size(), tracks.size());
      for (size_t i = 0; i < tracks.size(); ++i) {
        ASSERT_EQ(types[i], tracks[i]->GetMediaType());
        ASSERT_NE("", tracks[i]->GetStreamId());
        ASSERT_NE("", tracks[i]->GetTrackId());
        if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) {
          std::string msidAttr("a=msid:");
          msidAttr += tracks[i]->GetStreamId();
          msidAttr += " ";
          msidAttr += tracks[i]->GetTrackId();
          ASSERT_NE(std::string::npos, offer.find(msidAttr))
            << "Did not find " << msidAttr << " in offer";
        }
      }
    }
  }

  void
  SetLocalAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
  {
    nsresult rv = mSessionAns.SetLocalDescription(kJsepSdpAnswer, answer);
    if (checkFlags & CHECK_SUCCESS) {
      ASSERT_EQ(NS_OK, rv);
    }

    if (checkFlags & CHECK_TRACKS) {
      // Verify that the right stuff is in the tracks.
      auto pairs = mSessionAns.GetNegotiatedTrackPairs();
      ASSERT_EQ(types.size(), pairs.size());
      for (size_t i = 0; i < types.size(); ++i) {
        ASSERT_TRUE(pairs[i].mSending);
        ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType());
        ASSERT_TRUE(pairs[i].mReceiving);
        ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType());
        ASSERT_NE("", pairs[i].mSending->GetStreamId());
        ASSERT_NE("", pairs[i].mSending->GetTrackId());
        // These might have been in the SDP, or might have been randomly
        // chosen by JsepSessionImpl
        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());

        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
          std::string msidAttr("a=msid:");
          msidAttr += pairs[i].mSending->GetStreamId();
          msidAttr += " ";
          msidAttr += pairs[i].mSending->GetTrackId();
          ASSERT_NE(std::string::npos, answer.find(msidAttr))
            << "Did not find " << msidAttr << " in offer";
        }
      }
    }
    std::cerr << "OFFER pairs:" << std::endl;
    DumpTrackPairs(mSessionOff);
  }

  void
  SetRemoteAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
  {
    nsresult rv = mSessionOff.SetRemoteDescription(kJsepSdpAnswer, answer);
    if (checkFlags & CHECK_SUCCESS) {
      ASSERT_EQ(NS_OK, rv);
    }

    if (checkFlags & CHECK_TRACKS) {
      // Verify that the right stuff is in the tracks.
      auto pairs = mSessionOff.GetNegotiatedTrackPairs();
      ASSERT_EQ(types.size(), pairs.size());
      for (size_t i = 0; i < types.size(); ++i) {
        ASSERT_TRUE(pairs[i].mSending);
        ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType());
        ASSERT_TRUE(pairs[i].mReceiving);
        ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType());
        ASSERT_NE("", pairs[i].mSending->GetStreamId());
        ASSERT_NE("", pairs[i].mSending->GetTrackId());
        // These might have been in the SDP, or might have been randomly
        // chosen by JsepSessionImpl
        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());

        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
          std::string msidAttr("a=msid:");
          msidAttr += pairs[i].mReceiving->GetStreamId();
          msidAttr += " ";
          msidAttr += pairs[i].mReceiving->GetTrackId();
          ASSERT_NE(std::string::npos, answer.find(msidAttr))
            << "Did not find " << msidAttr << " in answer";
        }
      }
    }
    std::cerr << "ANSWER pairs:" << std::endl;
    DumpTrackPairs(mSessionAns);
  }

  typedef enum {
    RTP = 1,
    RTCP = 2
  } ComponentType;

  class CandidateSet {
    public:
      CandidateSet() {}

      void Gather(JsepSession& session,
                  const std::vector<SdpMediaSection::MediaType>& types,
                  ComponentType maxComponent = RTCP)
      {
        for (size_t level = 0; level < types.size(); ++level) {
          Gather(session, level, RTP);
          if (types[level] != SdpMediaSection::kApplication &&
              maxComponent == RTCP) {
            Gather(session, level, RTCP);
          }
        }
        FinishGathering(session);
      }

      void Gather(JsepSession& session, size_t level, ComponentType component)
      {
        static uint16_t port = 1000;
        std::vector<std::string> candidates;
        for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) {
          ++port;
          std::ostringstream candidate;
          candidate << "0 " << static_cast<uint16_t>(component)
                    << " UDP 9999 192.168.0.1 " << port << " typ host";
          std::string mid;
          bool skipped;
          session.AddLocalIceCandidate(kAEqualsCandidate + candidate.str(),
                                       level, &mid, &skipped);
          if (!skipped) {
            mCandidatesToTrickle.push_back(
                Tuple<Level, Mid, Candidate>(
                  level, mid, kAEqualsCandidate + candidate.str()));
            candidates.push_back(candidate.str());
          }
        }

        // Stomp existing candidates
        mCandidates[level][component] = candidates;

        // Stomp existing defaults
        mDefaultCandidates[level][component] =
          std::make_pair("192.168.0.1", port);
        session.UpdateDefaultCandidate(
            mDefaultCandidates[level][RTP].first,
            mDefaultCandidates[level][RTP].second,
            // Will be empty string if not present, which is how we indicate
            // that there is no default for RTCP
            mDefaultCandidates[level][RTCP].first,
            mDefaultCandidates[level][RTCP].second,
            level);
      }

      void FinishGathering(JsepSession& session) const
      {
        // Copy so we can be terse and use []
        for (auto levelAndCandidates : mDefaultCandidates) {
          ASSERT_EQ(1U, levelAndCandidates.second.count(RTP));
          // do a final UpdateDefaultCandidate here in case candidates were
          // cleared during renegotiation.
          session.UpdateDefaultCandidate(
              levelAndCandidates.second[RTP].first,
              levelAndCandidates.second[RTP].second,
              // Will be empty string if not present, which is how we indicate
              // that there is no default for RTCP
              levelAndCandidates.second[RTCP].first,
              levelAndCandidates.second[RTCP].second,
              levelAndCandidates.first);
          session.EndOfLocalCandidates(levelAndCandidates.first);
        }
      }

      void Trickle(JsepSession& session)
      {
        for (const auto& levelMidAndCandidate : mCandidatesToTrickle) {
          Level level;
          Mid mid;
          Candidate candidate;
          Tie(level, mid, candidate) = levelMidAndCandidate;
          session.AddRemoteIceCandidate(candidate, mid, level);
        }
        mCandidatesToTrickle.clear();
      }

      void CheckRtpCandidates(bool expectRtpCandidates,
                              const SdpMediaSection& msection,
                              size_t transportLevel,
                              const std::string& context) const
      {
        auto& attrs = msection.GetAttributeList();

        ASSERT_EQ(expectRtpCandidates,
                  attrs.HasAttribute(SdpAttribute::kCandidateAttribute))
          << context << " (level " << msection.GetLevel() << ")";

        if (expectRtpCandidates) {
          // Copy so we can be terse and use []
          auto expectedCandidates = mCandidates;
          ASSERT_LE(kNumCandidatesPerComponent,
                    expectedCandidates[transportLevel][RTP].size());

          auto& candidates = attrs.GetCandidate();
          ASSERT_LE(kNumCandidatesPerComponent, candidates.size())
            << context << " (level " << msection.GetLevel() << ")";
          for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) {
            ASSERT_EQ(expectedCandidates[transportLevel][RTP][i], candidates[i])
              << context << " (level " << msection.GetLevel() << ")";
          }
        }
      }

      void CheckRtcpCandidates(bool expectRtcpCandidates,
                               const SdpMediaSection& msection,
                               size_t transportLevel,
                               const std::string& context) const
      {
        auto& attrs = msection.GetAttributeList();

        if (expectRtcpCandidates) {
          // Copy so we can be terse and use []
          auto expectedCandidates = mCandidates;
          ASSERT_LE(kNumCandidatesPerComponent,
                    expectedCandidates[transportLevel][RTCP].size());

          ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kCandidateAttribute))
            << context << " (level " << msection.GetLevel() << ")";
          auto& candidates = attrs.GetCandidate();
          ASSERT_EQ(kNumCandidatesPerComponent * 2, candidates.size())
            << context << " (level " << msection.GetLevel() << ")";
          for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) {
            ASSERT_EQ(expectedCandidates[transportLevel][RTCP][i],
                      candidates[i + kNumCandidatesPerComponent])
              << context << " (level " << msection.GetLevel() << ")";
          }
        }
      }

      void CheckDefaultRtpCandidate(bool expectDefault,
                                    const SdpMediaSection& msection,
                                    size_t transportLevel,
                                    const std::string& context) const
      {
        if (expectDefault) {
          // Copy so we can be terse and use []
          auto defaultCandidates = mDefaultCandidates;
          ASSERT_EQ(defaultCandidates[transportLevel][RTP].first,
                    msection.GetConnection().GetAddress())
            << context << " (level " << msection.GetLevel() << ")";
          ASSERT_EQ(defaultCandidates[transportLevel][RTP].second,
                    msection.GetPort())
            << context << " (level " << msection.GetLevel() << ")";
        } else {
          ASSERT_EQ("0.0.0.0", msection.GetConnection().GetAddress())
            << context << " (level " << msection.GetLevel() << ")";
          ASSERT_EQ(9U, msection.GetPort())
            << context << " (level " << msection.GetLevel() << ")";
        }
      }

      void CheckDefaultRtcpCandidate(bool expectDefault,
                                     const SdpMediaSection& msection,
                                     size_t transportLevel,
                                     const std::string& context) const
      {
        if (expectDefault) {
          // Copy so we can be terse and use []
          auto defaultCandidates = mDefaultCandidates;
          ASSERT_TRUE(msection.GetAttributeList().HasAttribute(
                SdpAttribute::kRtcpAttribute))
            << context << " (level " << msection.GetLevel() << ")";
          auto& rtcpAttr = msection.GetAttributeList().GetRtcp();
          ASSERT_EQ(defaultCandidates[transportLevel][RTCP].second,
                    rtcpAttr.mPort)
            << context << " (level " << msection.GetLevel() << ")";
          ASSERT_EQ(sdp::kInternet, rtcpAttr.mNetType)
            << context << " (level " << msection.GetLevel() << ")";
          ASSERT_EQ(sdp::kIPv4, rtcpAttr.mAddrType)
            << context << " (level " << msection.GetLevel() << ")";
          ASSERT_EQ(defaultCandidates[transportLevel][RTCP].first,
                    rtcpAttr.mAddress)
            << context << " (level " << msection.GetLevel() << ")";
        } else {
          ASSERT_FALSE(msection.GetAttributeList().HasAttribute(
                SdpAttribute::kRtcpAttribute))
            << context << " (level " << msection.GetLevel() << ")";
        }
      }

    private:
      typedef size_t Level;
      typedef std::string Mid;
      typedef std::string Candidate;
      typedef std::string Address;
      typedef uint16_t Port;
      // Default candidates are put into the m-line, c-line, and rtcp
      // attribute for endpoints that don't support ICE.
      std::map<Level,
               std::map<ComponentType,
                        std::pair<Address, Port>>> mDefaultCandidates;
      std::map<Level,
               std::map<ComponentType,
                        std::vector<Candidate>>> mCandidates;
      // Level/mid/candidate tuples that need to be trickled
      std::vector<Tuple<Level, Mid, Candidate>> mCandidatesToTrickle;
  };

  // For streaming parse errors
  std::string
  GetParseErrors(const SipccSdpParser& parser) const
  {
    std::stringstream output;
    for (auto e = parser.GetParseErrors().begin();
         e != parser.GetParseErrors().end();
         ++e) {
      output << e->first << ": " << e->second << std::endl;
    }
    return output.str();
  }

  void CheckEndOfCandidates(bool expectEoc,
                            const SdpMediaSection& msection,
                            const std::string& context)
  {
    if (expectEoc) {
      ASSERT_TRUE(msection.GetAttributeList().HasAttribute(
            SdpAttribute::kEndOfCandidatesAttribute))
        << context << " (level " << msection.GetLevel() << ")";
    } else {
      ASSERT_FALSE(msection.GetAttributeList().HasAttribute(
            SdpAttribute::kEndOfCandidatesAttribute))
        << context << " (level " << msection.GetLevel() << ")";
    }
  }

  void CheckPairs(const JsepSession& session, const std::string& context)
  {
    auto pairs = session.GetNegotiatedTrackPairs();

    for (JsepTrackPair& pair : pairs) {
      if (types.size() == 1) {
        ASSERT_FALSE(pair.mBundleLevel.isSome()) << context;
      } else {
        ASSERT_TRUE(pair.mBundleLevel.isSome()) << context;
        ASSERT_EQ(0U, *pair.mBundleLevel) << context;
      }
    }
  }

  void
  DisableMsid(std::string* sdp) const {
    size_t pos = sdp->find("a=msid-semantic");
    ASSERT_NE(std::string::npos, pos);
    (*sdp)[pos + 2] = 'X'; // garble, a=Xsid-semantic
  }

  void
  DisableBundle(std::string* sdp) const {
    size_t pos = sdp->find("a=group:BUNDLE");
    ASSERT_NE(std::string::npos, pos);
    (*sdp)[pos + 11] = 'G'; // garble, a=group:BUNGLE
  }

  void
  DisableMsection(std::string* sdp, size_t level) const {
    UniquePtr<Sdp> parsed(Parse(*sdp));
    ASSERT_TRUE(parsed.get());
    ASSERT_LT(level, parsed->GetMediaSectionCount());
    SdpHelper::DisableMsection(parsed.get(), &parsed->GetMediaSection(level));
    (*sdp) = parsed->ToString();
  }

  void
  ReplaceInSdp(std::string* sdp,
               const char* searchStr,
               const char* replaceStr) const
  {
    if (searchStr[0] == '\0') return;
    size_t pos;
    while ((pos = sdp->find(searchStr)) != std::string::npos) {
      sdp->replace(pos, strlen(searchStr), replaceStr);
    }
  }

  void
  ValidateDisabledMSection(const SdpMediaSection* msection)
  {
    ASSERT_EQ(1U, msection->GetFormats().size());
    // Maybe validate that no attributes are present except rtpmap and
    // inactive? How?
    ASSERT_EQ(SdpDirectionAttribute::kInactive,
              msection->GetDirectionAttribute().mValue);
    if (msection->GetMediaType() == SdpMediaSection::kAudio) {
      ASSERT_EQ("0", msection->GetFormats()[0]);
      const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("0"));
      ASSERT_TRUE(rtpmap);
      ASSERT_EQ("0", rtpmap->pt);
      ASSERT_EQ("PCMU", rtpmap->name);
    } else if (msection->GetMediaType() == SdpMediaSection::kVideo) {
      ASSERT_EQ("120", msection->GetFormats()[0]);
      const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("120"));
      ASSERT_TRUE(rtpmap);
      ASSERT_EQ("120", rtpmap->pt);
      ASSERT_EQ("VP8", rtpmap->name);
    } else if (msection->GetMediaType() == SdpMediaSection::kApplication) {
      ASSERT_EQ("5000", msection->GetFormats()[0]);
      const SdpSctpmapAttributeList::Sctpmap* sctpmap(msection->FindSctpmap("5000"));
      ASSERT_TRUE(sctpmap);
      ASSERT_EQ("5000", sctpmap->pt);
      ASSERT_EQ("rejected", sctpmap->name);
      ASSERT_EQ(0U, sctpmap->streams);
    } else {
      // Not that we would have any test which tests this...
      ASSERT_EQ("19", msection->GetFormats()[0]);
      const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("19"));
      ASSERT_TRUE(rtpmap);
      ASSERT_EQ("19", rtpmap->pt);
      ASSERT_EQ("reserved", rtpmap->name);
    }
  }

  void
  DumpTrack(const JsepTrack& track)
  {
    const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
    std::cerr << "  type=" << track.GetMediaType() << std::endl;
    std::cerr << "  encodings=" << std::endl;
    for (size_t i = 0; i < details->GetEncodingCount(); ++i) {
      const JsepTrackEncoding& encoding = details->GetEncoding(i);
      std::cerr << "    id=" << encoding.mRid << std::endl;
      for (const JsepCodecDescription* codec : encoding.GetCodecs()) {
        std::cerr << "      " << codec->mName
                  << " enabled(" << (codec->mEnabled?"yes":"no") << ")";
        if (track.GetMediaType() == SdpMediaSection::kAudio) {
          const JsepAudioCodecDescription* audioCodec =
              static_cast<const JsepAudioCodecDescription*>(codec);
          std::cerr << " dtmf(" << (audioCodec->mDtmfEnabled?"yes":"no") << ")";
        }
        std::cerr << std::endl;
      }
    }
  }

  void
  DumpTrackPairs(const JsepSessionImpl& session)
  {
    auto pairs = mSessionAns.GetNegotiatedTrackPairs();
    for (auto i = pairs.begin(); i != pairs.end(); ++i) {
      std::cerr << "Track pair " << i->mLevel << std::endl;
      if (i->mSending) {
        std::cerr << "Sending-->" << std::endl;
        DumpTrack(*i->mSending);
      }
      if (i->mReceiving) {
        std::cerr << "Receiving-->" << std::endl;
        DumpTrack(*i->mReceiving);
      }
    }
  }

  UniquePtr<Sdp>
  Parse(const std::string& sdp) const
  {
    SipccSdpParser parser;
    UniquePtr<Sdp> parsed = parser.Parse(sdp);
    EXPECT_TRUE(parsed.get()) << "Should have valid SDP" << std::endl
                              << "Errors were: " << GetParseErrors(parser);
    return parsed;
  }

  JsepSessionImpl mSessionOff;
  CandidateSet mOffCandidates;
  JsepSessionImpl mSessionAns;
  CandidateSet mAnsCandidates;
  std::vector<SdpMediaSection::MediaType> types;
  std::vector<std::pair<std::string, uint16_t>> mGatheredCandidates;

private:
  void
  ValidateTransport(TransportData& source, const std::string& sdp_str)
  {
    UniquePtr<Sdp> sdp(Parse(sdp_str));
    ASSERT_TRUE(!!sdp);
    size_t num_m_sections = sdp->GetMediaSectionCount();
    for (size_t i = 0; i < num_m_sections; ++i) {
      auto& msection = sdp->GetMediaSection(i);

      if (msection.GetMediaType() == SdpMediaSection::kApplication) {
        ASSERT_EQ(SdpMediaSection::kDtlsSctp, msection.GetProtocol());
      } else {
        ASSERT_EQ(SdpMediaSection::kUdpTlsRtpSavpf, msection.GetProtocol());
      }

      if (msection.GetPort() == 0) {
        ValidateDisabledMSection(&msection);
        continue;
      }
      const SdpAttributeList& attrs = msection.GetAttributeList();
      ASSERT_EQ(source.mIceUfrag, attrs.GetIceUfrag());
      ASSERT_EQ(source.mIcePwd, attrs.GetIcePwd());
      const SdpFingerprintAttributeList& fps = attrs.GetFingerprint();
      for (auto fp = fps.mFingerprints.begin(); fp != fps.mFingerprints.end();
           ++fp) {
        std::string alg_str = "None";

        if (fp->hashFunc == SdpFingerprintAttributeList::kSha1) {
          alg_str = "sha-1";
        } else if (fp->hashFunc == SdpFingerprintAttributeList::kSha256) {
          alg_str = "sha-256";
        }

        ASSERT_EQ(source.mFingerprints[alg_str], fp->fingerprint);
      }
      ASSERT_EQ(source.mFingerprints.size(), fps.mFingerprints.size());
    }
  }

  TransportData mOffererTransport;
  TransportData mAnswererTransport;
};

TEST_F(JsepSessionTestBase, CreateDestroy) {}

TEST_P(JsepSessionTest, CreateOffer)
{
  AddTracks(mSessionOff);
  CreateOffer();
}

TEST_P(JsepSessionTest, CreateOfferSetLocal)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
}

TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemote)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
}

TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswer)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
}

TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswerSetLocal)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
}

TEST_P(JsepSessionTest, FullCall)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);
}

TEST_P(JsepSessionTest, RenegotiationNoChange)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(types.size(), added.size());
  ASSERT_EQ(0U, removed.size());

  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(types.size(), added.size());
  ASSERT_EQ(0U, removed.size());

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::string reoffer = CreateOffer();
  SetLocalOffer(reoffer);
  SetRemoteOffer(reoffer);

  added = mSessionAns.GetRemoteTracksAdded();
  removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(0U, removed.size());

  std::string reanswer = CreateAnswer();
  SetLocalAnswer(reanswer);
  SetRemoteAnswer(reanswer);

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(0U, removed.size());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
  for (size_t i = 0; i < answererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::vector<SdpMediaSection::MediaType> extraTypes;
  extraTypes.push_back(SdpMediaSection::kAudio);
  extraTypes.push_back(SdpMediaSection::kVideo);
  AddTracks(mSessionOff, extraTypes);
  types.insert(types.end(), extraTypes.begin(), extraTypes.end());

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(2U, added.size());
  ASSERT_EQ(0U, removed.size());
  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(0U, removed.size());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
  for (size_t i = 0; i < answererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationAnswererAddsTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::vector<SdpMediaSection::MediaType> extraTypes;
  extraTypes.push_back(SdpMediaSection::kAudio);
  extraTypes.push_back(SdpMediaSection::kVideo);
  AddTracks(mSessionAns, extraTypes);
  types.insert(types.end(), extraTypes.begin(), extraTypes.end());

  // We need to add a recvonly m-section to the offer for this to work
  JsepOfferOptions options;
  options.mOfferToReceiveAudio =
    Some(GetTrackCount(mSessionOff, SdpMediaSection::kAudio) + 1);
  options.mOfferToReceiveVideo =
    Some(GetTrackCount(mSessionOff, SdpMediaSection::kVideo) + 1);

  std::string offer = CreateOffer(Some(options));
  SetLocalOffer(offer, CHECK_SUCCESS);
  SetRemoteOffer(offer, CHECK_SUCCESS);

  std::string answer = CreateAnswer();
  SetLocalAnswer(answer, CHECK_SUCCESS);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(0U, removed.size());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(2U, added.size());
  ASSERT_EQ(0U, removed.size());
  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
  for (size_t i = 0; i < answererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationBothAddTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::vector<SdpMediaSection::MediaType> extraTypes;
  extraTypes.push_back(SdpMediaSection::kAudio);
  extraTypes.push_back(SdpMediaSection::kVideo);
  AddTracks(mSessionAns, extraTypes);
  AddTracks(mSessionOff, extraTypes);
  types.insert(types.end(), extraTypes.begin(), extraTypes.end());

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(2U, added.size());
  ASSERT_EQ(0U, removed.size());
  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(2U, added.size());
  ASSERT_EQ(0U, removed.size());
  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
  for (size_t i = 0; i < answererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationBothAddTracksToExistingStream)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  if (GetParam() == "datachannel") {
    return;
  }

  OfferAnswer();

  auto oHasStream = HasMediaStream(mSessionOff.GetLocalTracks());
  auto aHasStream = HasMediaStream(mSessionAns.GetLocalTracks());
  ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(mSessionOff).size());
  ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(mSessionAns).size());
  ASSERT_EQ(aHasStream, GetRemoteUniqueStreamIds(mSessionOff).size());
  ASSERT_EQ(oHasStream, GetRemoteUniqueStreamIds(mSessionAns).size());

  auto firstOffId = GetFirstLocalStreamId(mSessionOff);
  auto firstAnsId = GetFirstLocalStreamId(mSessionAns);

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::vector<SdpMediaSection::MediaType> extraTypes;
  extraTypes.push_back(SdpMediaSection::kAudio);
  extraTypes.push_back(SdpMediaSection::kVideo);
  AddTracksToStream(mSessionOff, firstOffId, extraTypes);
  AddTracksToStream(mSessionAns, firstAnsId, extraTypes);
  types.insert(types.end(), extraTypes.begin(), extraTypes.end());

  OfferAnswer(CHECK_SUCCESS);

  oHasStream = HasMediaStream(mSessionOff.GetLocalTracks());
  aHasStream = HasMediaStream(mSessionAns.GetLocalTracks());

  ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(mSessionOff).size());
  ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(mSessionAns).size());
  ASSERT_EQ(aHasStream, GetRemoteUniqueStreamIds(mSessionOff).size());
  ASSERT_EQ(oHasStream, GetRemoteUniqueStreamIds(mSessionAns).size());
  if (oHasStream) {
    ASSERT_STREQ(firstOffId.c_str(),
                 GetFirstLocalStreamId(mSessionOff).c_str());
  }
  if (aHasStream) {
    ASSERT_STREQ(firstAnsId.c_str(),
                 GetFirstLocalStreamId(mSessionAns).c_str());
  }
}

TEST_P(JsepSessionTest, RenegotiationOffererRemovesTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  if (types.front() == SdpMediaSection::kApplication) {
    return;
  }

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
  ASSERT_TRUE(removedTrack);
  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrack->GetStreamId(),
                                           removedTrack->GetTrackId()));

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(1U, removed.size());

  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(0U, removed.size());

  // First m-section should be recvonly
  auto offer = GetParsedLocalDescription(mSessionOff);
  auto* msection = GetMsection(*offer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_FALSE(msection->IsSending());

  // First audio m-section should be sendonly
  auto answer = GetParsedLocalDescription(mSessionAns);
  msection = GetMsection(*answer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_FALSE(msection->IsReceiving());
  ASSERT_TRUE(msection->IsSending());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  // Will be the same size since we still have a track on one side.
  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());

  // This should be the only difference.
  ASSERT_TRUE(offererPairs[0].mSending);
  ASSERT_FALSE(newOffererPairs[0].mSending);

  // Remove this difference, let loop below take care of the rest
  offererPairs[0].mSending = nullptr;
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  // Will be the same size since we still have a track on one side.
  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());

  // This should be the only difference.
  ASSERT_TRUE(answererPairs[0].mReceiving);
  ASSERT_FALSE(newAnswererPairs[0].mReceiving);

  // Remove this difference, let loop below take care of the rest
  answererPairs[0].mReceiving = nullptr;
  for (size_t i = 0; i < answererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationAnswererRemovesTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  if (types.front() == SdpMediaSection::kApplication) {
    return;
  }

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  RefPtr<JsepTrack> removedTrack = GetTrackAns(0, types.front());
  ASSERT_TRUE(removedTrack);
  ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrack->GetStreamId(),
                                           removedTrack->GetTrackId()));

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(0U, removed.size());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(1U, removed.size());

  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());

  // First m-section should be sendrecv
  auto offer = GetParsedLocalDescription(mSessionOff);
  auto* msection = GetMsection(*offer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_TRUE(msection->IsSending());

  // First audio m-section should be recvonly
  auto answer = GetParsedLocalDescription(mSessionAns);
  msection = GetMsection(*answer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_FALSE(msection->IsSending());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  // Will be the same size since we still have a track on one side.
  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());

  // This should be the only difference.
  ASSERT_TRUE(offererPairs[0].mReceiving);
  ASSERT_FALSE(newOffererPairs[0].mReceiving);

  // Remove this difference, let loop below take care of the rest
  offererPairs[0].mReceiving = nullptr;
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  // Will be the same size since we still have a track on one side.
  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());

  // This should be the only difference.
  ASSERT_TRUE(answererPairs[0].mSending);
  ASSERT_FALSE(newAnswererPairs[0].mSending);

  // Remove this difference, let loop below take care of the rest
  answererPairs[0].mSending = nullptr;
  for (size_t i = 0; i < answererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationBothRemoveTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  if (types.front() == SdpMediaSection::kApplication) {
    return;
  }

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
  ASSERT_TRUE(removedTrackAnswer);
  ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(),
                                           removedTrackAnswer->GetTrackId()));

  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(0, types.front());
  ASSERT_TRUE(removedTrackOffer);
  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(),
                                           removedTrackOffer->GetTrackId()));

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(1U, removed.size());

  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(1U, removed.size());

  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());

  // First m-section should be recvonly
  auto offer = GetParsedLocalDescription(mSessionOff);
  auto* msection = GetMsection(*offer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_FALSE(msection->IsSending());

  // First m-section should be inactive, and rejected
  auto answer = GetParsedLocalDescription(mSessionAns);
  msection = GetMsection(*answer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_FALSE(msection->IsReceiving());
  ASSERT_FALSE(msection->IsSending());
  ASSERT_FALSE(msection->GetPort());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);

  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
    JsepTrackPair oldPair(offererPairs[i + 1]);
    JsepTrackPair newPair(newOffererPairs[i]);
    ASSERT_EQ(oldPair.mLevel, newPair.mLevel);
    ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get());
    ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get());
    ASSERT_TRUE(oldPair.mBundleLevel.isSome());
    ASSERT_TRUE(newPair.mBundleLevel.isSome());
    ASSERT_EQ(0U, *oldPair.mBundleLevel);
    ASSERT_EQ(1U, *newPair.mBundleLevel);
  }

  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);

  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
    JsepTrackPair oldPair(answererPairs[i + 1]);
    JsepTrackPair newPair(newAnswererPairs[i]);
    ASSERT_EQ(oldPair.mLevel, newPair.mLevel);
    ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get());
    ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get());
    ASSERT_TRUE(oldPair.mBundleLevel.isSome());
    ASSERT_TRUE(newPair.mBundleLevel.isSome());
    ASSERT_EQ(0U, *oldPair.mBundleLevel);
    ASSERT_EQ(1U, *newPair.mBundleLevel);
  }
}

TEST_P(JsepSessionTest, RenegotiationBothRemoveThenAddTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  if (types.front() == SdpMediaSection::kApplication) {
    return;
  }

  SdpMediaSection::MediaType removedType = types.front();

  OfferAnswer();

  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, removedType);
  ASSERT_TRUE(removedTrackAnswer);
  ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(),
                                           removedTrackAnswer->GetTrackId()));

  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(0, removedType);
  ASSERT_TRUE(removedTrackOffer);
  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(),
                                           removedTrackOffer->GetTrackId()));

  OfferAnswer(CHECK_SUCCESS);

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::vector<SdpMediaSection::MediaType> extraTypes;
  extraTypes.push_back(removedType);
  AddTracks(mSessionAns, extraTypes);
  AddTracks(mSessionOff, extraTypes);
  types.insert(types.end(), extraTypes.begin(), extraTypes.end());

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(1U, added.size());
  ASSERT_EQ(0U, removed.size());
  ASSERT_EQ(removedType, added[0]->GetMediaType());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(1U, added.size());
  ASSERT_EQ(0U, removed.size());
  ASSERT_EQ(removedType, added[0]->GetMediaType());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size() + 1, newOffererPairs.size());
  ASSERT_EQ(answererPairs.size() + 1, newAnswererPairs.size());

  // Ensure that the m-section was re-used; no gaps
  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
    ASSERT_EQ(i, newOffererPairs[i].mLevel);
  }
  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
    ASSERT_EQ(i, newAnswererPairs[i].mLevel);
  }
}

TEST_P(JsepSessionTest, RenegotiationBothRemoveTrackDifferentMsection)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  if (types.front() == SdpMediaSection::kApplication) {
    return;
  }

  if (types.size() < 2 || types[0] != types[1]) {
    // For simplicity, just run in cases where we have two of the same type
    return;
  }

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
  ASSERT_TRUE(removedTrackAnswer);
  ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(),
                                           removedTrackAnswer->GetTrackId()));

  // Second instance of the same type
  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(1, types.front());
  ASSERT_TRUE(removedTrackOffer);
  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(),
                                           removedTrackOffer->GetTrackId()));

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(1U, removed.size());

  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(1U, removed.size());

  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());

  // Second m-section should be recvonly
  auto offer = GetParsedLocalDescription(mSessionOff);
  auto* msection = GetMsection(*offer, types.front(), 1);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_FALSE(msection->IsSending());

  // First m-section should be recvonly
  auto answer = GetParsedLocalDescription(mSessionAns);
  msection = GetMsection(*answer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_FALSE(msection->IsSending());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());

  // This should be the only difference.
  ASSERT_TRUE(offererPairs[0].mReceiving);
  ASSERT_FALSE(newOffererPairs[0].mReceiving);

  // Remove this difference, let loop below take care of the rest
  offererPairs[0].mReceiving = nullptr;

  // This should be the only difference.
  ASSERT_TRUE(offererPairs[1].mSending);
  ASSERT_FALSE(newOffererPairs[1].mSending);

  // Remove this difference, let loop below take care of the rest
  offererPairs[1].mSending = nullptr;

  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());

  // This should be the only difference.
  ASSERT_TRUE(answererPairs[0].mSending);
  ASSERT_FALSE(newAnswererPairs[0].mSending);

  // Remove this difference, let loop below take care of the rest
  answererPairs[0].mSending = nullptr;

  // This should be the only difference.
  ASSERT_TRUE(answererPairs[1].mReceiving);
  ASSERT_FALSE(newAnswererPairs[1].mReceiving);

  // Remove this difference, let loop below take care of the rest
  answererPairs[1].mReceiving = nullptr;

  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationOffererReplacesTrack)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  if (types.front() == SdpMediaSection::kApplication) {
    return;
  }

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
  ASSERT_TRUE(removedTrack);
  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrack->GetStreamId(),
                                           removedTrack->GetTrackId()));
  RefPtr<JsepTrack> addedTrack(
      new JsepTrack(types.front(), "newstream", "newtrack"));
  ASSERT_EQ(NS_OK, mSessionOff.AddTrack(addedTrack));

  OfferAnswer(CHECK_SUCCESS);

  auto added = mSessionAns.GetRemoteTracksAdded();
  auto removed = mSessionAns.GetRemoteTracksRemoved();
  ASSERT_EQ(1U, added.size());
  ASSERT_EQ(1U, removed.size());

  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());

  ASSERT_EQ(addedTrack->GetMediaType(), added[0]->GetMediaType());
  ASSERT_EQ(addedTrack->GetStreamId(), added[0]->GetStreamId());
  ASSERT_EQ(addedTrack->GetTrackId(), added[0]->GetTrackId());

  added = mSessionOff.GetRemoteTracksAdded();
  removed = mSessionOff.GetRemoteTracksRemoved();
  ASSERT_EQ(0U, added.size());
  ASSERT_EQ(0U, removed.size());

  // First audio m-section should be sendrecv
  auto offer = GetParsedLocalDescription(mSessionOff);
  auto* msection = GetMsection(*offer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_TRUE(msection->IsSending());

  // First audio m-section should be sendrecv
  auto answer = GetParsedLocalDescription(mSessionAns);
  msection = GetMsection(*answer, types.front(), 0);
  ASSERT_TRUE(msection);
  ASSERT_TRUE(msection->IsReceiving());
  ASSERT_TRUE(msection->IsSending());

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());

  ASSERT_NE(offererPairs[0].mSending->GetStreamId(),
            newOffererPairs[0].mSending->GetStreamId());
  ASSERT_NE(offererPairs[0].mSending->GetTrackId(),
            newOffererPairs[0].mSending->GetTrackId());

  // Skip first pair
  for (size_t i = 1; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }

  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());

  ASSERT_NE(answererPairs[0].mReceiving->GetStreamId(),
            newAnswererPairs[0].mReceiving->GetStreamId());
  ASSERT_NE(answererPairs[0].mReceiving->GetTrackId(),
            newAnswererPairs[0].mReceiving->GetTrackId());

  // Skip first pair
  for (size_t i = 1; i < newAnswererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
  }
}

// Tests whether auto-assigned remote msids (ie; what happens when the other
// side doesn't use msid attributes) are stable across renegotiation.
TEST_P(JsepSessionTest, RenegotiationAutoAssignedMsidIsStable)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);

  DisableMsid(&answer);

  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);

  // Make sure that DisableMsid actually worked, since it is kinda hacky
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
  ASSERT_EQ(offererPairs.size(), answererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(offererPairs[i].mReceiving);
    ASSERT_TRUE(answererPairs[i].mSending);
    // These should not match since we've monkeyed with the msid
    ASSERT_NE(offererPairs[i].mReceiving->GetStreamId(),
              answererPairs[i].mSending->GetStreamId());
    ASSERT_NE(offererPairs[i].mReceiving->GetTrackId(),
              answererPairs[i].mSending->GetTrackId());
  }

  offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  answer = CreateAnswer();
  SetLocalAnswer(answer);

  DisableMsid(&answer);

  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs();

  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
  }
}

TEST_P(JsepSessionTest, RenegotiationOffererDisablesTelephoneEvent)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);

  // check all the audio tracks to make sure they have 2 codecs (109 and 101),
  // and dtmf is enabled on all audio tracks
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    std::vector<JsepTrack*> tracks;
    tracks.push_back(offererPairs[i].mSending.get());
    tracks.push_back(offererPairs[i].mReceiving.get());
    for (JsepTrack *track : tracks) {
      if (track->GetMediaType() != SdpMediaSection::kAudio) {
        continue;
      }
      const JsepTrackNegotiatedDetails* details = track->GetNegotiatedDetails();
      ASSERT_EQ(1U, details->GetEncodingCount());
      const JsepTrackEncoding& encoding = details->GetEncoding(0);
      ASSERT_EQ(2U, encoding.GetCodecs().size());
      ASSERT_TRUE(encoding.HasFormat("109"));
      ASSERT_TRUE(encoding.HasFormat("101"));
      for (JsepCodecDescription* codec: encoding.GetCodecs()) {
        ASSERT_TRUE(codec);
        // we can cast here because we've already checked for audio track
        JsepAudioCodecDescription *audioCodec =
            static_cast<JsepAudioCodecDescription*>(codec);
        ASSERT_TRUE(audioCodec->mDtmfEnabled);
      }
    }
  }

  std::string offer = CreateOffer();
  ReplaceInSdp(&offer, " 109 101 ", " 109 ");
  ReplaceInSdp(&offer, "a=fmtp:101 0-15\r\n", "");
  ReplaceInSdp(&offer, "a=rtpmap:101 telephone-event/8000/1\r\n", "");
  std::cerr << "modified OFFER: " << offer << std::endl;

  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);

  // check all the audio tracks to make sure they have 1 codec (109),
  // and dtmf is disabled on all audio tracks
  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
    std::vector<JsepTrack*> tracks;
    tracks.push_back(newOffererPairs[i].mSending.get());
    tracks.push_back(newOffererPairs[i].mReceiving.get());
    for (JsepTrack* track : tracks) {
      if (track->GetMediaType() != SdpMediaSection::kAudio) {
        continue;
      }
      const JsepTrackNegotiatedDetails* details = track->GetNegotiatedDetails();
      ASSERT_EQ(1U, details->GetEncodingCount());
      const JsepTrackEncoding& encoding = details->GetEncoding(0);
      ASSERT_EQ(1U, encoding.GetCodecs().size());
      ASSERT_TRUE(encoding.HasFormat("109"));
      // we can cast here because we've already checked for audio track
      JsepAudioCodecDescription *audioCodec =
          static_cast<JsepAudioCodecDescription*>(encoding.GetCodecs()[0]);
      ASSERT_TRUE(audioCodec);
      ASSERT_FALSE(audioCodec->mDtmfEnabled);
    }
  }
}

// Tests behavior when the answerer does not use msid in the initial exchange,
// but does on renegotiation.
TEST_P(JsepSessionTest, RenegotiationAnswererEnablesMsid)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);

  DisableMsid(&answer);

  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);

  offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs();

  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
              newOffererPairs[i].mReceiving->GetMediaType());

    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
    ASSERT_TRUE(Equals(offererPairs[i].mRtpTransport,
                       newOffererPairs[i].mRtpTransport));
    ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport,
                       newOffererPairs[i].mRtcpTransport));

    if (offererPairs[i].mReceiving->GetMediaType() ==
        SdpMediaSection::kApplication) {
      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
    } else {
      // This should be the only difference
      ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
    }
  }
}

TEST_P(JsepSessionTest, RenegotiationAnswererDisablesMsid)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);

  offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  AddTracks(mSessionAns);
  answer = CreateAnswer();
  SetLocalAnswer(answer);

  DisableMsid(&answer);

  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs();

  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
  for (size_t i = 0; i < offererPairs.size(); ++i) {
    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
              newOffererPairs[i].mReceiving->GetMediaType());

    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
    ASSERT_TRUE(Equals(offererPairs[i].mRtpTransport,
                       newOffererPairs[i].mRtpTransport));
    ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport,
                       newOffererPairs[i].mRtcpTransport));

    if (offererPairs[i].mReceiving->GetMediaType() ==
        SdpMediaSection::kApplication) {
      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
    } else {
      // This should be the only difference
      ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
    }
  }
}

// Tests behavior when offerer does not use bundle on the initial offer/answer,
// but does on renegotiation.
TEST_P(JsepSessionTest, RenegotiationOffererEnablesBundle)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  if (types.size() < 2) {
    // No bundle will happen here.
    return;
  }

  std::string offer = CreateOffer();

  DisableBundle(&offer);

  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  OfferAnswer();

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());

  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
    // No bundle initially
    ASSERT_FALSE(offererPairs[i].mBundleLevel.isSome());
    ASSERT_FALSE(answererPairs[i].mBundleLevel.isSome());
    if (i != 0) {
      ASSERT_NE(offererPairs[0].mRtpTransport.get(),
                offererPairs[i].mRtpTransport.get());
      if (offererPairs[0].mRtcpTransport) {
        ASSERT_NE(offererPairs[0].mRtcpTransport.get(),
                  offererPairs[i].mRtcpTransport.get());
      }
      ASSERT_NE(answererPairs[0].mRtpTransport.get(),
                answererPairs[i].mRtpTransport.get());
      if (answererPairs[0].mRtcpTransport) {
        ASSERT_NE(answererPairs[0].mRtcpTransport.get(),
                  answererPairs[i].mRtcpTransport.get());
      }
    }

    // Verify that bundle worked after renegotiation
    ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome());
    ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome());
    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
              newOffererPairs[i].mRtpTransport.get());
    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
              newOffererPairs[i].mRtcpTransport.get());
    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
              newAnswererPairs[i].mRtpTransport.get());
    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
              newAnswererPairs[i].mRtcpTransport.get());
  }
}

TEST_P(JsepSessionTest, RenegotiationOffererDisablesBundleTransport)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  if (types.size() < 2) {
    return;
  }

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::string reoffer = CreateOffer();

  DisableMsection(&reoffer, 0);

  SetLocalOffer(reoffer, CHECK_SUCCESS);
  SetRemoteOffer(reoffer, CHECK_SUCCESS);
  std::string reanswer = CreateAnswer();
  SetLocalAnswer(reanswer, CHECK_SUCCESS);
  SetRemoteAnswer(reanswer, CHECK_SUCCESS);

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);

  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
    ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome());
    ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome());
    ASSERT_EQ(1U, *newOffererPairs[i].mBundleLevel);
    ASSERT_EQ(1U, *newAnswererPairs[i].mBundleLevel);
    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
              newOffererPairs[i].mRtpTransport.get());
    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
              newOffererPairs[i].mRtcpTransport.get());
    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
              newAnswererPairs[i].mRtpTransport.get());
    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
              newAnswererPairs[i].mRtcpTransport.get());
  }

  ASSERT_NE(newOffererPairs[0].mRtpTransport.get(),
            offererPairs[0].mRtpTransport.get());
  ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(),
            answererPairs[0].mRtpTransport.get());

  ASSERT_LE(1U, mSessionOff.GetTransports().size());
  ASSERT_LE(1U, mSessionAns.GetTransports().size());

  ASSERT_EQ(0U, mSessionOff.GetTransports()[0]->mComponents);
  ASSERT_EQ(0U, mSessionAns.GetTransports()[0]->mComponents);
}

TEST_P(JsepSessionTest, RenegotiationAnswererDisablesBundleTransport)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  if (types.size() < 2) {
    return;
  }

  OfferAnswer();

  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
  auto answererPairs = GetTrackPairsByLevel(mSessionAns);

  std::string reoffer = CreateOffer();
  SetLocalOffer(reoffer, CHECK_SUCCESS);
  SetRemoteOffer(reoffer, CHECK_SUCCESS);
  std::string reanswer = CreateAnswer();

  DisableMsection(&reanswer, 0);

  SetLocalAnswer(reanswer, CHECK_SUCCESS);
  SetRemoteAnswer(reanswer, CHECK_SUCCESS);

  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);

  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);

  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
    ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome());
    ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome());
    ASSERT_EQ(1U, *newOffererPairs[i].mBundleLevel);
    ASSERT_EQ(1U, *newAnswererPairs[i].mBundleLevel);
    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
              newOffererPairs[i].mRtpTransport.get());
    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
              newOffererPairs[i].mRtcpTransport.get());
    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
              newAnswererPairs[i].mRtpTransport.get());
    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
              newAnswererPairs[i].mRtcpTransport.get());
  }

  ASSERT_NE(newOffererPairs[0].mRtpTransport.get(),
            offererPairs[0].mRtpTransport.get());
  ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(),
            answererPairs[0].mRtpTransport.get());
}

TEST_P(JsepSessionTest, ParseRejectsBadMediaFormat)
{
  if (GetParam() == "datachannel") {
    return;
  }
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  UniquePtr<Sdp> munge(Parse(offer));
  SdpMediaSection& mediaSection = munge->GetMediaSection(0);
  mediaSection.AddCodec("75", "DummyFormatVal", 8000, 1);
  std::string sdpString = munge->ToString();
  nsresult rv = mSessionOff.SetLocalDescription(kJsepSdpOffer, sdpString);
  ASSERT_EQ(NS_ERROR_INVALID_ARG, rv);
}

TEST_P(JsepSessionTest, FullCallWithCandidates)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  mOffCandidates.Gather(mSessionOff, types);

  UniquePtr<Sdp> localOffer(Parse(mSessionOff.GetLocalDescription()));
  for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) {
    mOffCandidates.CheckRtpCandidates(
        true, localOffer->GetMediaSection(i), i,
        "Local offer after gathering should have RTP candidates.");
    mOffCandidates.CheckDefaultRtpCandidate(
        true, localOffer->GetMediaSection(i), i,
        "Local offer after gathering should have a default RTP candidate.");
    mOffCandidates.CheckRtcpCandidates(
        types[i] != SdpMediaSection::kApplication,
        localOffer->GetMediaSection(i), i,
        "Local offer after gathering should have RTCP candidates "
        "(unless m=application)");
    mOffCandidates.CheckDefaultRtcpCandidate(
        types[i] != SdpMediaSection::kApplication,
        localOffer->GetMediaSection(i), i,
        "Local offer after gathering should have a default RTCP candidate "
        "(unless m=application)");
    CheckEndOfCandidates(true, localOffer->GetMediaSection(i),
        "Local offer after gathering should have an end-of-candidates.");
  }

  SetRemoteOffer(offer);
  mOffCandidates.Trickle(mSessionAns);

  UniquePtr<Sdp> remoteOffer(Parse(mSessionAns.GetRemoteDescription()));
  for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) {
    mOffCandidates.CheckRtpCandidates(
        true, remoteOffer->GetMediaSection(i), i,
        "Remote offer after trickle should have RTP candidates.");
    mOffCandidates.CheckDefaultRtpCandidate(
        false, remoteOffer->GetMediaSection(i), i,
        "Initial remote offer should not have a default RTP candidate.");
    mOffCandidates.CheckRtcpCandidates(
        types[i] != SdpMediaSection::kApplication,
        remoteOffer->GetMediaSection(i), i,
        "Remote offer after trickle should have RTCP candidates "
        "(unless m=application)");
    mOffCandidates.CheckDefaultRtcpCandidate(
        false, remoteOffer->GetMediaSection(i), i,
        "Initial remote offer should not have a default RTCP candidate.");
    CheckEndOfCandidates(false, remoteOffer->GetMediaSection(i),
        "Initial remote offer should not have an end-of-candidates.");
  }

  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  // This will gather candidates that mSessionAns knows it doesn't need.
  // They should not be present in the SDP.
  mAnsCandidates.Gather(mSessionAns, types);

  UniquePtr<Sdp> localAnswer(Parse(mSessionAns.GetLocalDescription()));
  for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) {
    mAnsCandidates.CheckRtpCandidates(
        i == 0, localAnswer->GetMediaSection(i), i,
        "Local answer after gathering should have RTP candidates on level 0.");
    mAnsCandidates.CheckDefaultRtpCandidate(
        true, localAnswer->GetMediaSection(i), 0,
        "Local answer after gathering should have a default RTP candidate "
        "on all levels that matches transport level 0.");
    mAnsCandidates.CheckRtcpCandidates(
        false, localAnswer->GetMediaSection(i), i,
        "Local answer after gathering should not have RTCP candidates "
        "(because we're answering with rtcp-mux)");
    mAnsCandidates.CheckDefaultRtcpCandidate(
        false, localAnswer->GetMediaSection(i), i,
        "Local answer after gathering should not have a default RTCP candidate "
        "(because we're answering with rtcp-mux)");
    CheckEndOfCandidates(i == 0, localAnswer->GetMediaSection(i),
        "Local answer after gathering should have an end-of-candidates only for"
        " level 0.");
  }

  SetRemoteAnswer(answer);
  mAnsCandidates.Trickle(mSessionOff);

  UniquePtr<Sdp> remoteAnswer(Parse(mSessionOff.GetRemoteDescription()));
  for (size_t i = 0; i < remoteAnswer->GetMediaSectionCount(); ++i) {
    mAnsCandidates.CheckRtpCandidates(
        i == 0, remoteAnswer->GetMediaSection(i), i,
        "Remote answer after trickle should have RTP candidates on level 0.");
    mAnsCandidates.CheckDefaultRtpCandidate(
        false, remoteAnswer->GetMediaSection(i), i,
        "Remote answer after trickle should not have a default RTP candidate.");
    mAnsCandidates.CheckRtcpCandidates(
        false, remoteAnswer->GetMediaSection(i), i,
        "Remote answer after trickle should not have RTCP candidates "
        "(because we're answering with rtcp-mux)");
    mAnsCandidates.CheckDefaultRtcpCandidate(
        false, remoteAnswer->GetMediaSection(i), i,
        "Remote answer after trickle should not have a default RTCP "
        "candidate.");
    CheckEndOfCandidates(false, remoteAnswer->GetMediaSection(i),
        "Remote answer after trickle should not have an end-of-candidates.");
  }
}

TEST_P(JsepSessionTest, RenegotiationWithCandidates)
{
  AddTracks(mSessionOff);
  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  mOffCandidates.Gather(mSessionOff, types);
  SetRemoteOffer(offer);
  mOffCandidates.Trickle(mSessionAns);
  AddTracks(mSessionAns);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  mAnsCandidates.Gather(mSessionAns, types);
  SetRemoteAnswer(answer);
  mAnsCandidates.Trickle(mSessionOff);

  offer = CreateOffer();
  SetLocalOffer(offer);

  UniquePtr<Sdp> parsedOffer(Parse(offer));
  for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) {
    mOffCandidates.CheckRtpCandidates(
        i == 0, parsedOffer->GetMediaSection(i), i,
        "Local reoffer before gathering should have RTP candidates on level 0"
        " only.");
    mOffCandidates.CheckDefaultRtpCandidate(
        i == 0, parsedOffer->GetMediaSection(i), 0,
        "Local reoffer before gathering should have a default RTP candidate "
        "on level 0 only.");
    mOffCandidates.CheckRtcpCandidates(
        false, parsedOffer->GetMediaSection(i), i,
        "Local reoffer before gathering should not have RTCP candidates.");
    mOffCandidates.CheckDefaultRtcpCandidate(
        false, parsedOffer->GetMediaSection(i), i,
        "Local reoffer before gathering should not have a default RTCP "
        "candidate.");
    CheckEndOfCandidates(false, parsedOffer->GetMediaSection(i),
        "Local reoffer before gathering should not have an end-of-candidates.");
  }

  // mSessionAns should generate a reoffer that is similar
  std::string otherOffer;
  JsepOfferOptions defaultOptions;
  nsresult rv = mSessionAns.CreateOffer(defaultOptions, &otherOffer);
  ASSERT_EQ(NS_OK, rv);
  parsedOffer = Parse(otherOffer);
  for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) {
    mAnsCandidates.CheckRtpCandidates(
        i == 0, parsedOffer->GetMediaSection(i), i,
        "Local reoffer before gathering should have RTP candidates on level 0"
        " only. (previous answerer)");
    mAnsCandidates.CheckDefaultRtpCandidate(
        i == 0, parsedOffer->GetMediaSection(i), 0,
        "Local reoffer before gathering should have a default RTP candidate "
        "on level 0 only. (previous answerer)");
    mAnsCandidates.CheckRtcpCandidates(
        false, parsedOffer->GetMediaSection(i), i,
        "Local reoffer before gathering should not have RTCP candidates."
        " (previous answerer)");
    mAnsCandidates.CheckDefaultRtcpCandidate(
        false, parsedOffer->GetMediaSection(i), i,
        "Local reoffer before gathering should not have a default RTCP "
        "candidate. (previous answerer)");
    CheckEndOfCandidates(false, parsedOffer->GetMediaSection(i),
        "Local reoffer before gathering should not have an end-of-candidates. "
        "(previous answerer)");
  }

  // Ok, let's continue with the renegotiation
  SetRemoteOffer(offer);

  // PeerConnection will not re-gather for RTP, but it will for RTCP in case
  // the answerer decides to turn off rtcp-mux.
  if (types[0] != SdpMediaSection::kApplication) {
    mOffCandidates.Gather(mSessionOff, 0, RTCP);
  }

  // Since the remaining levels were bundled, PeerConnection will re-gather for
  // both RTP and RTCP, in case the answerer rejects bundle.
  for (size_t level = 1; level < types.size(); ++level) {
    mOffCandidates.Gather(mSessionOff, level, RTP);
    if (types[level] != SdpMediaSection::kApplication) {
      mOffCandidates.Gather(mSessionOff, level, RTCP);
    }
  }
  mOffCandidates.FinishGathering(mSessionOff);

  mOffCandidates.Trickle(mSessionAns);

  UniquePtr<Sdp> localOffer(Parse(mSessionOff.GetLocalDescription()));
  for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) {
    mOffCandidates.CheckRtpCandidates(
        true, localOffer->GetMediaSection(i), i,
        "Local reoffer after gathering should have RTP candidates.");
    mOffCandidates.CheckDefaultRtpCandidate(
        true, localOffer->GetMediaSection(i), i,
        "Local reoffer after gathering should have a default RTP candidate.");
    mOffCandidates.CheckRtcpCandidates(
        types[i] != SdpMediaSection::kApplication,
        localOffer->GetMediaSection(i), i,
        "Local reoffer after gathering should have RTCP candidates "
        "(unless m=application)");
    mOffCandidates.CheckDefaultRtcpCandidate(
        types[i] != SdpMediaSection::kApplication,
        localOffer->GetMediaSection(i), i,
        "Local reoffer after gathering should have a default RTCP candidate "
        "(unless m=application)");
    CheckEndOfCandidates(true, localOffer->GetMediaSection(i),
        "Local reoffer after gathering should have an end-of-candidates.");
  }

  UniquePtr<Sdp> remoteOffer(Parse(mSessionAns.GetRemoteDescription()));
  for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) {
    mOffCandidates.CheckRtpCandidates(
        true, remoteOffer->GetMediaSection(i), i,
        "Remote reoffer after trickle should have RTP candidates.");
    mOffCandidates.CheckDefaultRtpCandidate(
        i == 0, remoteOffer->GetMediaSection(i), i,
        "Remote reoffer should have a default RTP candidate on level 0 "
        "(because it was gathered last offer/answer).");
    mOffCandidates.CheckRtcpCandidates(
        types[i] != SdpMediaSection::kApplication,
        remoteOffer->GetMediaSection(i), i,
        "Remote reoffer after trickle should have RTCP candidates.");
    mOffCandidates.CheckDefaultRtcpCandidate(
        false, remoteOffer->GetMediaSection(i), i,
        "Remote reoffer should not have a default RTCP candidate.");
    CheckEndOfCandidates(false, remoteOffer->GetMediaSection(i),
        "Remote reoffer should not have an end-of-candidates.");
  }

  answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);
  // No candidates should be gathered at the answerer, but default candidates
  // should be set.
  mAnsCandidates.FinishGathering(mSessionAns);

  UniquePtr<Sdp> localAnswer(Parse(mSessionAns.GetLocalDescription()));
  for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) {
    mAnsCandidates.CheckRtpCandidates(
        i == 0, localAnswer->GetMediaSection(i), i,
        "Local reanswer after gathering should have RTP candidates on level "
        "0.");
    mAnsCandidates.CheckDefaultRtpCandidate(
        true, localAnswer->GetMediaSection(i), 0,
        "Local reanswer after gathering should have a default RTP candidate "
        "on all levels that matches transport level 0.");
    mAnsCandidates.CheckRtcpCandidates(
        false, localAnswer->GetMediaSection(i), i,
        "Local reanswer after gathering should not have RTCP candidates "
        "(because we're reanswering with rtcp-mux)");
    mAnsCandidates.CheckDefaultRtcpCandidate(
        false, localAnswer->GetMediaSection(i), i,
        "Local reanswer after gathering should not have a default RTCP "
        "candidate (because we're reanswering with rtcp-mux)");
    CheckEndOfCandidates(i == 0, localAnswer->GetMediaSection(i),
        "Local reanswer after gathering should have an end-of-candidates only "
        "for level 0.");
  }

  UniquePtr<Sdp> remoteAnswer(Parse(mSessionOff.GetRemoteDescription()));
  for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) {
    mAnsCandidates.CheckRtpCandidates(
        i == 0, remoteAnswer->GetMediaSection(i), i,
        "Remote reanswer after trickle should have RTP candidates on level 0.");
    mAnsCandidates.CheckDefaultRtpCandidate(
        i == 0, remoteAnswer->GetMediaSection(i), i,
        "Remote reanswer should have a default RTP candidate on level 0 "
        "(because it was gathered last offer/answer).");
    mAnsCandidates.CheckRtcpCandidates(
        false, remoteAnswer->GetMediaSection(i), i,
        "Remote reanswer after trickle should not have RTCP candidates "
        "(because we're reanswering with rtcp-mux)");
    mAnsCandidates.CheckDefaultRtcpCandidate(
        false, remoteAnswer->GetMediaSection(i), i,
        "Remote reanswer after trickle should not have a default RTCP "
        "candidate.");
    CheckEndOfCandidates(false, remoteAnswer->GetMediaSection(i),
        "Remote reanswer after trickle should not have an end-of-candidates.");
  }
}

TEST_P(JsepSessionTest, RenegotiationAnswererSendonly)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  OfferAnswer();

  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);

  UniquePtr<Sdp> parsedAnswer(Parse(answer));
  for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) {
    SdpMediaSection& msection = parsedAnswer->GetMediaSection(i);
    if (msection.GetMediaType() != SdpMediaSection::kApplication) {
      msection.SetReceiving(false);
    }
  }

  answer = parsedAnswer->ToString();

  SetRemoteAnswer(answer);

  for (const RefPtr<JsepTrack>& track : mSessionOff.GetLocalTracks()) {
    if (track->GetMediaType() != SdpMediaSection::kApplication) {
      ASSERT_FALSE(track->GetActive());
    }
  }

  ASSERT_EQ(types.size(), mSessionOff.GetNegotiatedTrackPairs().size());
}

TEST_P(JsepSessionTest, RenegotiationAnswererInactive)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  OfferAnswer();

  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);

  UniquePtr<Sdp> parsedAnswer(Parse(answer));
  for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) {
    SdpMediaSection& msection = parsedAnswer->GetMediaSection(i);
    if (msection.GetMediaType() != SdpMediaSection::kApplication) {
      msection.SetReceiving(false);
      msection.SetSending(false);
    }
  }

  answer = parsedAnswer->ToString();

  SetRemoteAnswer(answer, CHECK_SUCCESS); // Won't have answerer tracks

  for (const RefPtr<JsepTrack>& track : mSessionOff.GetLocalTracks()) {
    if (track->GetMediaType() != SdpMediaSection::kApplication) {
      ASSERT_FALSE(track->GetActive());
    }
  }

  ASSERT_EQ(types.size(), mSessionOff.GetNegotiatedTrackPairs().size());
}


INSTANTIATE_TEST_CASE_P(
    Variants,
    JsepSessionTest,
    ::testing::Values("audio",
                      "video",
                      "datachannel",
                      "audio,video",
                      "video,audio",
                      "audio,datachannel",
                      "video,datachannel",
                      "video,audio,datachannel",
                      "audio,video,datachannel",
                      "datachannel,audio",
                      "datachannel,video",
                      "datachannel,audio,video",
                      "datachannel,video,audio",
                      "audio,datachannel,video",
                      "video,datachannel,audio",
                      "audio,audio",
                      "video,video",
                      "audio,audio,video",
                      "audio,video,video",
                      "audio,audio,video,video",
                      "audio,audio,video,video,datachannel"));

// offerToReceiveXxx variants

TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines)
{
  JsepOfferOptions options;
  options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
  options.mOfferToReceiveVideo = Some(static_cast<size_t>(2U));
  options.mDontOfferDataChannel = Some(true);
  std::string offer = CreateOffer(Some(options));

  UniquePtr<Sdp> parsedOffer(Parse(offer));
  ASSERT_TRUE(!!parsedOffer);

  ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kAudio,
            parsedOffer->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
            parsedOffer->GetMediaSection(0).GetAttributeList().GetDirection());
  ASSERT_TRUE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute(
        SdpAttribute::kSsrcAttribute));

  ASSERT_EQ(SdpMediaSection::kVideo,
            parsedOffer->GetMediaSection(1).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
            parsedOffer->GetMediaSection(1).GetAttributeList().GetDirection());
  ASSERT_TRUE(parsedOffer->GetMediaSection(1).GetAttributeList().HasAttribute(
        SdpAttribute::kSsrcAttribute));

  ASSERT_EQ(SdpMediaSection::kVideo,
            parsedOffer->GetMediaSection(2).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
            parsedOffer->GetMediaSection(2).GetAttributeList().GetDirection());
  ASSERT_TRUE(parsedOffer->GetMediaSection(2).GetAttributeList().HasAttribute(
        SdpAttribute::kSsrcAttribute));

  ASSERT_TRUE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute(
      SdpAttribute::kRtcpMuxAttribute));
  ASSERT_TRUE(parsedOffer->GetMediaSection(1).GetAttributeList().HasAttribute(
      SdpAttribute::kRtcpMuxAttribute));
  ASSERT_TRUE(parsedOffer->GetMediaSection(2).GetAttributeList().HasAttribute(
      SdpAttribute::kRtcpMuxAttribute));

  SetLocalOffer(offer, CHECK_SUCCESS);

  AddTracks(mSessionAns, "audio,video");
  SetRemoteOffer(offer, CHECK_SUCCESS);

  std::string answer = CreateAnswer();
  UniquePtr<Sdp> parsedAnswer(Parse(answer));

  ASSERT_EQ(3U, parsedAnswer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kAudio,
            parsedAnswer->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
            parsedAnswer->GetMediaSection(0).GetAttributeList().GetDirection());
  ASSERT_EQ(SdpMediaSection::kVideo,
            parsedAnswer->GetMediaSection(1).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
            parsedAnswer->GetMediaSection(1).GetAttributeList().GetDirection());
  ASSERT_EQ(SdpMediaSection::kVideo,
            parsedAnswer->GetMediaSection(2).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kInactive,
            parsedAnswer->GetMediaSection(2).GetAttributeList().GetDirection());

  SetLocalAnswer(answer, CHECK_SUCCESS);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  std::vector<JsepTrackPair> trackPairs(mSessionOff.GetNegotiatedTrackPairs());
  ASSERT_EQ(2U, trackPairs.size());
  for (auto pair : trackPairs) {
    auto ssrcs = parsedOffer->GetMediaSection(pair.mLevel).GetAttributeList()
                 .GetSsrc().mSsrcs;
    ASSERT_EQ(1U, ssrcs.size());
    ASSERT_EQ(pair.mRecvonlySsrc, ssrcs.front().ssrc);
  }
}

TEST_F(JsepSessionTest, OfferAnswerSendOnlyLines)
{
  AddTracks(mSessionOff, "audio,video,video");

  JsepOfferOptions options;
  options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U));
  options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U));
  options.mDontOfferDataChannel = Some(true);
  std::string offer = CreateOffer(Some(options));

  UniquePtr<Sdp> outputSdp(Parse(offer));
  ASSERT_TRUE(!!outputSdp);

  ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kAudio,
            outputSdp->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
            outputSdp->GetMediaSection(0).GetAttributeList().GetDirection());
  ASSERT_EQ(SdpMediaSection::kVideo,
            outputSdp->GetMediaSection(1).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kSendrecv,
            outputSdp->GetMediaSection(1).GetAttributeList().GetDirection());
  ASSERT_EQ(SdpMediaSection::kVideo,
            outputSdp->GetMediaSection(2).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
            outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());

  ASSERT_TRUE(outputSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
      SdpAttribute::kRtcpMuxAttribute));
  ASSERT_TRUE(outputSdp->GetMediaSection(1).GetAttributeList().HasAttribute(
      SdpAttribute::kRtcpMuxAttribute));
  ASSERT_TRUE(outputSdp->GetMediaSection(2).GetAttributeList().HasAttribute(
      SdpAttribute::kRtcpMuxAttribute));

  SetLocalOffer(offer, CHECK_SUCCESS);

  AddTracks(mSessionAns, "audio,video");
  SetRemoteOffer(offer, CHECK_SUCCESS);

  std::string answer = CreateAnswer();
  outputSdp = Parse(answer);

  ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kAudio,
            outputSdp->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
            outputSdp->GetMediaSection(0).GetAttributeList().GetDirection());
  ASSERT_EQ(SdpMediaSection::kVideo,
            outputSdp->GetMediaSection(1).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kSendrecv,
            outputSdp->GetMediaSection(1).GetAttributeList().GetDirection());
  ASSERT_EQ(SdpMediaSection::kVideo,
            outputSdp->GetMediaSection(2).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
            outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());
}

TEST_F(JsepSessionTest, OfferToReceiveAudioNotUsed)
{
  JsepOfferOptions options;
  options.mOfferToReceiveAudio = Some<size_t>(1);

  OfferAnswer(CHECK_SUCCESS, Some(options));

  UniquePtr<Sdp> offer(Parse(mSessionOff.GetLocalDescription()));
  ASSERT_TRUE(offer.get());
  ASSERT_EQ(1U, offer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kAudio,
            offer->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
            offer->GetMediaSection(0).GetAttributeList().GetDirection());

  UniquePtr<Sdp> answer(Parse(mSessionAns.GetLocalDescription()));
  ASSERT_TRUE(answer.get());
  ASSERT_EQ(1U, answer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kAudio,
            answer->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kInactive,
            answer->GetMediaSection(0).GetAttributeList().GetDirection());
}

TEST_F(JsepSessionTest, OfferToReceiveVideoNotUsed)
{
  JsepOfferOptions options;
  options.mOfferToReceiveVideo = Some<size_t>(1);

  OfferAnswer(CHECK_SUCCESS, Some(options));

  UniquePtr<Sdp> offer(Parse(mSessionOff.GetLocalDescription()));
  ASSERT_TRUE(offer.get());
  ASSERT_EQ(1U, offer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kVideo,
            offer->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
            offer->GetMediaSection(0).GetAttributeList().GetDirection());

  UniquePtr<Sdp> answer(Parse(mSessionAns.GetLocalDescription()));
  ASSERT_TRUE(answer.get());
  ASSERT_EQ(1U, answer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kVideo,
            answer->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpDirectionAttribute::kInactive,
            answer->GetMediaSection(0).GetAttributeList().GetDirection());
}

TEST_F(JsepSessionTest, CreateOfferNoDatachannelDefault)
{
  RefPtr<JsepTrack> msta(
      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
  mSessionOff.AddTrack(msta);

  RefPtr<JsepTrack> mstv1(
      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
  mSessionOff.AddTrack(mstv1);

  std::string offer = CreateOffer();

  UniquePtr<Sdp> outputSdp(Parse(offer));
  ASSERT_TRUE(!!outputSdp);

  ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kAudio,
            outputSdp->GetMediaSection(0).GetMediaType());
  ASSERT_EQ(SdpMediaSection::kVideo,
            outputSdp->GetMediaSection(1).GetMediaType());
}

TEST_F(JsepSessionTest, ValidateOfferedVideoCodecParams)
{
  types.push_back(SdpMediaSection::kAudio);
  types.push_back(SdpMediaSection::kVideo);

  RefPtr<JsepTrack> msta(
      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
  mSessionOff.AddTrack(msta);
  RefPtr<JsepTrack> mstv1(
      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2"));
  mSessionOff.AddTrack(mstv1);

  std::string offer = CreateOffer();

  UniquePtr<Sdp> outputSdp(Parse(offer));
  ASSERT_TRUE(!!outputSdp);

  ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
  auto& video_section = outputSdp->GetMediaSection(1);
  ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType());
  auto& video_attrs = video_section.GetAttributeList();
  ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection());

  ASSERT_EQ(6U, video_section.GetFormats().size());
  ASSERT_EQ("120", video_section.GetFormats()[0]);
  ASSERT_EQ("121", video_section.GetFormats()[1]);
  ASSERT_EQ("126", video_section.GetFormats()[2]);
  ASSERT_EQ("97", video_section.GetFormats()[3]);
  ASSERT_EQ("122", video_section.GetFormats()[4]);
  ASSERT_EQ("123", video_section.GetFormats()[5]);

  // Validate rtpmap
  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute));
  auto& rtpmaps = video_attrs.GetRtpmap();
  ASSERT_TRUE(rtpmaps.HasEntry("120"));
  ASSERT_TRUE(rtpmaps.HasEntry("121"));
  ASSERT_TRUE(rtpmaps.HasEntry("126"));
  ASSERT_TRUE(rtpmaps.HasEntry("97"));
  ASSERT_TRUE(rtpmaps.HasEntry("122"));
  ASSERT_TRUE(rtpmaps.HasEntry("123"));

  auto& vp8_entry = rtpmaps.GetEntry("120");
  auto& vp9_entry = rtpmaps.GetEntry("121");
  auto& h264_1_entry = rtpmaps.GetEntry("126");
  auto& h264_0_entry = rtpmaps.GetEntry("97");
  auto& red_0_entry = rtpmaps.GetEntry("122");
  auto& ulpfec_0_entry = rtpmaps.GetEntry("123");

  ASSERT_EQ("VP8", vp8_entry.name);
  ASSERT_EQ("VP9", vp9_entry.name);
  ASSERT_EQ("H264", h264_1_entry.name);
  ASSERT_EQ("H264", h264_0_entry.name);
  ASSERT_EQ("red", red_0_entry.name);
  ASSERT_EQ("ulpfec", ulpfec_0_entry.name);

  // Validate fmtps
  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute));
  auto& fmtps = video_attrs.GetFmtp().mFmtps;

  ASSERT_EQ(5U, fmtps.size());

  // VP8
  const SdpFmtpAttributeList::Parameters* vp8_params =
    video_section.FindFmtp("120");
  ASSERT_TRUE(vp8_params);
  ASSERT_EQ(SdpRtpmapAttributeList::kVP8, vp8_params->codec_type);

  auto& parsed_vp8_params =
      *static_cast<const SdpFmtpAttributeList::VP8Parameters*>(vp8_params);

  ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs);
  ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr);

  // VP9
  const SdpFmtpAttributeList::Parameters* vp9_params =
    video_section.FindFmtp("121");
  ASSERT_TRUE(vp9_params);
  ASSERT_EQ(SdpRtpmapAttributeList::kVP9, vp9_params->codec_type);

  auto& parsed_vp9_params =
      *static_cast<const SdpFmtpAttributeList::VP8Parameters*>(vp9_params);

  ASSERT_EQ((uint32_t)12288, parsed_vp9_params.max_fs);
  ASSERT_EQ((uint32_t)60, parsed_vp9_params.max_fr);

  // H264 packetization mode 1
  const SdpFmtpAttributeList::Parameters* h264_1_params =
    video_section.FindFmtp("126");
  ASSERT_TRUE(h264_1_params);
  ASSERT_EQ(SdpRtpmapAttributeList::kH264, h264_1_params->codec_type);

  auto& parsed_h264_1_params =
      *static_cast<const SdpFmtpAttributeList::H264Parameters*>(h264_1_params);

  ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_1_params.profile_level_id);
  ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed);
  ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode);

  // H264 packetization mode 0
  const SdpFmtpAttributeList::Parameters* h264_0_params =
    video_section.FindFmtp("97");
  ASSERT_TRUE(h264_0_params);
  ASSERT_EQ(SdpRtpmapAttributeList::kH264, h264_0_params->codec_type);

  auto& parsed_h264_0_params =
      *static_cast<const SdpFmtpAttributeList::H264Parameters*>(h264_0_params);

  ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_0_params.profile_level_id);
  ASSERT_TRUE(parsed_h264_0_params.level_asymmetry_allowed);
  ASSERT_EQ(0U, parsed_h264_0_params.packetization_mode);

  // red
  const SdpFmtpAttributeList::Parameters* red_params =
    video_section.FindFmtp("122");
  ASSERT_TRUE(red_params);
  ASSERT_EQ(SdpRtpmapAttributeList::kRed, red_params->codec_type);

  auto& parsed_red_params =
      *static_cast<const SdpFmtpAttributeList::RedParameters*>(red_params);
  ASSERT_EQ(5U, parsed_red_params.encodings.size());
  ASSERT_EQ(120, parsed_red_params.encodings[0]);
  ASSERT_EQ(121, parsed_red_params.encodings[1]);
  ASSERT_EQ(126, parsed_red_params.encodings[2]);
  ASSERT_EQ(97, parsed_red_params.encodings[3]);
  ASSERT_EQ(123, parsed_red_params.encodings[4]);
}

TEST_F(JsepSessionTest, ValidateOfferedAudioCodecParams)
{
  types.push_back(SdpMediaSection::kAudio);
  types.push_back(SdpMediaSection::kVideo);

  RefPtr<JsepTrack> msta(
      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
  mSessionOff.AddTrack(msta);
  RefPtr<JsepTrack> mstv1(
      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2"));
  mSessionOff.AddTrack(mstv1);

  std::string offer = CreateOffer();

  UniquePtr<Sdp> outputSdp(Parse(offer));
  ASSERT_TRUE(!!outputSdp);

  ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
  auto& audio_section = outputSdp->GetMediaSection(0);
  ASSERT_EQ(SdpMediaSection::kAudio, audio_section.GetMediaType());
  auto& audio_attrs = audio_section.GetAttributeList();
  ASSERT_EQ(SdpDirectionAttribute::kSendrecv, audio_attrs.GetDirection());
  ASSERT_EQ(5U, audio_section.GetFormats().size());
  ASSERT_EQ("109", audio_section.GetFormats()[0]);
  ASSERT_EQ("9", audio_section.GetFormats()[1]);
  ASSERT_EQ("0", audio_section.GetFormats()[2]);
  ASSERT_EQ("8", audio_section.GetFormats()[3]);
  ASSERT_EQ("101", audio_section.GetFormats()[4]);

  // Validate rtpmap
  ASSERT_TRUE(audio_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute));
  auto& rtpmaps = audio_attrs.GetRtpmap();
  ASSERT_TRUE(rtpmaps.HasEntry("109"));
  ASSERT_TRUE(rtpmaps.HasEntry("9"));
  ASSERT_TRUE(rtpmaps.HasEntry("0"));
  ASSERT_TRUE(rtpmaps.HasEntry("8"));
  ASSERT_TRUE(rtpmaps.HasEntry("101"));

  auto& opus_entry = rtpmaps.GetEntry("109");
  auto& g722_entry = rtpmaps.GetEntry("9");
  auto& pcmu_entry = rtpmaps.GetEntry("0");
  auto& pcma_entry = rtpmaps.GetEntry("8");
  auto& telephone_event_entry = rtpmaps.GetEntry("101");

  ASSERT_EQ("opus", opus_entry.name);
  ASSERT_EQ("G722", g722_entry.name);
  ASSERT_EQ("PCMU", pcmu_entry.name);
  ASSERT_EQ("PCMA", pcma_entry.name);
  ASSERT_EQ("telephone-event", telephone_event_entry.name);

  // Validate fmtps
  ASSERT_TRUE(audio_attrs.HasAttribute(SdpAttribute::kFmtpAttribute));
  auto& fmtps = audio_attrs.GetFmtp().mFmtps;

  ASSERT_EQ(2U, fmtps.size());

  // opus
  const SdpFmtpAttributeList::Parameters* opus_params =
    audio_section.FindFmtp("109");
  ASSERT_TRUE(opus_params);
  ASSERT_EQ(SdpRtpmapAttributeList::kOpus, opus_params->codec_type);

  auto& parsed_opus_params =
      *static_cast<const SdpFmtpAttributeList::OpusParameters*>(opus_params);

  ASSERT_EQ((uint32_t)48000, parsed_opus_params.maxplaybackrate);
  ASSERT_EQ((uint32_t)1, parsed_opus_params.stereo);
  ASSERT_EQ((uint32_t)0, parsed_opus_params.useInBandFec);

  // dtmf
  const SdpFmtpAttributeList::Parameters* dtmf_params =
    audio_section.FindFmtp("101");
  ASSERT_TRUE(dtmf_params);
  ASSERT_EQ(SdpRtpmapAttributeList::kTelephoneEvent, dtmf_params->codec_type);

  auto& parsed_dtmf_params =
      *static_cast<const SdpFmtpAttributeList::TelephoneEventParameters*>
          (dtmf_params);

  ASSERT_EQ("0-15", parsed_dtmf_params.dtmfTones);
}

TEST_F(JsepSessionTest, ValidateAnsweredCodecParams)
{
  // TODO(bug 1099351): Once fixed, we can allow red in this offer,
  // which will also cause multiple codecs in answer.  For now,
  // red/ulpfec for video are behind a pref to mitigate potential for
  // errors.
  SetCodecEnabled(mSessionOff, "red", false);
  for (auto i = mSessionAns.Codecs().begin(); i != mSessionAns.Codecs().end();
       ++i) {
    auto* codec = *i;
    if (codec->mName == "H264") {
      JsepVideoCodecDescription* h264 =
          static_cast<JsepVideoCodecDescription*>(codec);
      h264->mProfileLevelId = 0x42a00d;
      // Switch up the pts
      if (h264->mDefaultPt == "126") {
        h264->mDefaultPt = "97";
      } else {
        h264->mDefaultPt = "126";
      }
    }
  }

  types.push_back(SdpMediaSection::kAudio);
  types.push_back(SdpMediaSection::kVideo);

  RefPtr<JsepTrack> msta(
      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
  mSessionOff.AddTrack(msta);
  RefPtr<JsepTrack> mstv1(
      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
  mSessionOff.AddTrack(mstv1);

  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);

  RefPtr<JsepTrack> msta_ans(
      new JsepTrack(SdpMediaSection::kAudio, "answerer_stream", "a1"));
  mSessionAns.AddTrack(msta);
  RefPtr<JsepTrack> mstv1_ans(
      new JsepTrack(SdpMediaSection::kVideo, "answerer_stream", "v1"));
  mSessionAns.AddTrack(mstv1);

  std::string answer = CreateAnswer();

  UniquePtr<Sdp> outputSdp(Parse(answer));
  ASSERT_TRUE(!!outputSdp);

  ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
  auto& video_section = outputSdp->GetMediaSection(1);
  ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType());
  auto& video_attrs = video_section.GetAttributeList();
  ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection());

  // TODO(bug 1099351): Once fixed, this stuff will need to be updated.
  ASSERT_EQ(1U, video_section.GetFormats().size());
  // ASSERT_EQ(3U, video_section.GetFormats().size());
  ASSERT_EQ("120", video_section.GetFormats()[0]);
  // ASSERT_EQ("121", video_section.GetFormats()[1]);
  // ASSERT_EQ("126", video_section.GetFormats()[2]);
  // ASSERT_EQ("97", video_section.GetFormats()[3]);

  // Validate rtpmap
  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute));
  auto& rtpmaps = video_attrs.GetRtpmap();
  ASSERT_TRUE(rtpmaps.HasEntry("120"));
  //ASSERT_TRUE(rtpmaps.HasEntry("121"));
  // ASSERT_TRUE(rtpmaps.HasEntry("126"));
  // ASSERT_TRUE(rtpmaps.HasEntry("97"));

  auto& vp8_entry = rtpmaps.GetEntry("120");
  //auto& vp9_entry = rtpmaps.GetEntry("121");
  // auto& h264_1_entry = rtpmaps.GetEntry("126");
  // auto& h264_0_entry = rtpmaps.GetEntry("97");

  ASSERT_EQ("VP8", vp8_entry.name);
  //ASSERT_EQ("VP9", vp9_entry.name);
  // ASSERT_EQ("H264", h264_1_entry.name);
  // ASSERT_EQ("H264", h264_0_entry.name);

  // Validate fmtps
  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute));
  auto& fmtps = video_attrs.GetFmtp().mFmtps;

  ASSERT_EQ(1U, fmtps.size());
  // ASSERT_EQ(3U, fmtps.size());

  // VP8
  ASSERT_EQ("120", fmtps[0].format);
  ASSERT_TRUE(!!fmtps[0].parameters);
  ASSERT_EQ(SdpRtpmapAttributeList::kVP8, fmtps[0].parameters->codec_type);

  auto& parsed_vp8_params =
      *static_cast<const SdpFmtpAttributeList::VP8Parameters*>(
          fmtps[0].parameters.get());

  ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs);
  ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr);


  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);

  auto offerPairs = mSessionOff.GetNegotiatedTrackPairs();
  ASSERT_EQ(2U, offerPairs.size());
  ASSERT_TRUE(offerPairs[1].mSending);
  ASSERT_TRUE(offerPairs[1].mReceiving);
  ASSERT_TRUE(offerPairs[1].mSending->GetNegotiatedDetails());
  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
  ASSERT_EQ(1U,
      offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
      .GetCodecs().size());
  ASSERT_EQ(1U,
      offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
      .GetCodecs().size());

  auto answerPairs = mSessionAns.GetNegotiatedTrackPairs();
  ASSERT_EQ(2U, answerPairs.size());
  ASSERT_TRUE(answerPairs[1].mSending);
  ASSERT_TRUE(answerPairs[1].mReceiving);
  ASSERT_TRUE(answerPairs[1].mSending->GetNegotiatedDetails());
  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
  ASSERT_EQ(1U,
      answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
      .GetCodecs().size());
  ASSERT_EQ(1U,
      answerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
      .GetCodecs().size());

#if 0
  // H264 packetization mode 1
  ASSERT_EQ("126", fmtps[1].format);
  ASSERT_TRUE(fmtps[1].parameters);
  ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[1].parameters->codec_type);

  auto& parsed_h264_1_params =
    *static_cast<const SdpFmtpAttributeList::H264Parameters*>(
        fmtps[1].parameters.get());

  ASSERT_EQ((uint32_t)0x42a00d, parsed_h264_1_params.profile_level_id);
  ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed);
  ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode);

  // H264 packetization mode 0
  ASSERT_EQ("97", fmtps[2].format);
  ASSERT_TRUE(fmtps[2].parameters);
  ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[2].parameters->codec_type);

  auto& parsed_h264_0_params =
    *static_cast<const SdpFmtpAttributeList::H264Parameters*>(
        fmtps[2].parameters.get());

  ASSERT_EQ((uint32_t)0x42a00d, parsed_h264_0_params.profile_level_id);
  ASSERT_TRUE(parsed_h264_0_params.level_asymmetry_allowed);
  ASSERT_EQ(0U, parsed_h264_0_params.packetization_mode);
#endif
}

static void
Replace(const std::string& toReplace,
        const std::string& with,
        std::string* in)
{
  size_t pos = in->find(toReplace);
  ASSERT_NE(std::string::npos, pos);
  in->replace(pos, toReplace.size(), with);
}

static void ReplaceAll(const std::string& toReplace,
                       const std::string& with,
                       std::string* in)
{
  while (in->find(toReplace) != std::string::npos) {
    Replace(toReplace, with, in);
  }
}

static void
GetCodec(JsepSession& session,
         size_t pairIndex,
         sdp::Direction direction,
         size_t encodingIndex,
         size_t codecIndex,
         const JsepCodecDescription** codecOut)
{
  *codecOut = nullptr;
  ASSERT_LT(pairIndex, session.GetNegotiatedTrackPairs().size());
  JsepTrackPair pair(session.GetNegotiatedTrackPairs().front());
  RefPtr<JsepTrack> track(
      (direction == sdp::kSend) ? pair.mSending : pair.mReceiving);
  ASSERT_TRUE(track);
  ASSERT_TRUE(track->GetNegotiatedDetails());
  ASSERT_LT(encodingIndex, track->GetNegotiatedDetails()->GetEncodingCount());
  ASSERT_LT(codecIndex,
      track->GetNegotiatedDetails()->GetEncoding(encodingIndex)
      .GetCodecs().size());
  *codecOut =
      track->GetNegotiatedDetails()->GetEncoding(encodingIndex)
      .GetCodecs()[codecIndex];
}

static void
ForceH264(JsepSession& session, uint32_t profileLevelId)
{
  for (JsepCodecDescription* codec : session.Codecs()) {
    if (codec->mName == "H264") {
      JsepVideoCodecDescription* h264 =
          static_cast<JsepVideoCodecDescription*>(codec);
      h264->mProfileLevelId = profileLevelId;
    } else {
      codec->mEnabled = false;
    }
  }
}

TEST_F(JsepSessionTest, TestH264Negotiation)
{
  ForceH264(mSessionOff, 0x42e00b);
  ForceH264(mSessionAns, 0x42e00d);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);

  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  const JsepCodecDescription* offererSendCodec;
  GetCodec(mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec);
  ASSERT_TRUE(offererSendCodec);
  ASSERT_EQ("H264", offererSendCodec->mName);
  const JsepVideoCodecDescription* offererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(offererSendCodec));
  ASSERT_EQ((uint32_t)0x42e00d, offererVideoSendCodec->mProfileLevelId);

  const JsepCodecDescription* offererRecvCodec;
  GetCodec(mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec);
  ASSERT_EQ("H264", offererRecvCodec->mName);
  const JsepVideoCodecDescription* offererVideoRecvCodec(
      static_cast<const JsepVideoCodecDescription*>(offererRecvCodec));
  ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId);

  const JsepCodecDescription* answererSendCodec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec);
  ASSERT_TRUE(answererSendCodec);
  ASSERT_EQ("H264", answererSendCodec->mName);
  const JsepVideoCodecDescription* answererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(answererSendCodec));
  ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId);

  const JsepCodecDescription* answererRecvCodec;
  GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec);
  ASSERT_EQ("H264", answererRecvCodec->mName);
  const JsepVideoCodecDescription* answererVideoRecvCodec(
      static_cast<const JsepVideoCodecDescription*>(answererRecvCodec));
  ASSERT_EQ((uint32_t)0x42e00d, answererVideoRecvCodec->mProfileLevelId);
}

TEST_F(JsepSessionTest, TestH264NegotiationFails)
{
  ForceH264(mSessionOff, 0x42000b);
  ForceH264(mSessionAns, 0x42e00d);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);

  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  ASSERT_EQ(0U, mSessionOff.GetNegotiatedTrackPairs().size());
  ASSERT_EQ(0U, mSessionAns.GetNegotiatedTrackPairs().size());
}

TEST_F(JsepSessionTest, TestH264NegotiationOffererDefault)
{
  ForceH264(mSessionOff, 0x42000d);
  ForceH264(mSessionAns, 0x42000d);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);

  Replace("profile-level-id=42000d",
          "some-unknown-param=0",
          &offer);

  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  const JsepCodecDescription* answererSendCodec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec);
  ASSERT_TRUE(answererSendCodec);
  ASSERT_EQ("H264", answererSendCodec->mName);
  const JsepVideoCodecDescription* answererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(answererSendCodec));
  ASSERT_EQ((uint32_t)0x420010, answererVideoSendCodec->mProfileLevelId);
}

TEST_F(JsepSessionTest, TestH264NegotiationOffererNoFmtp)
{
  ForceH264(mSessionOff, 0x42000d);
  ForceH264(mSessionAns, 0x42001e);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);

  Replace("a=fmtp", "a=oops", &offer);

  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  const JsepCodecDescription* answererSendCodec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec);
  ASSERT_TRUE(answererSendCodec);
  ASSERT_EQ("H264", answererSendCodec->mName);
  const JsepVideoCodecDescription* answererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(answererSendCodec));
  ASSERT_EQ((uint32_t)0x420010, answererVideoSendCodec->mProfileLevelId);

  const JsepCodecDescription* answererRecvCodec;
  GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec);
  ASSERT_EQ("H264", answererRecvCodec->mName);
  const JsepVideoCodecDescription* answererVideoRecvCodec(
      static_cast<const JsepVideoCodecDescription*>(answererRecvCodec));
  ASSERT_EQ((uint32_t)0x420010, answererVideoRecvCodec->mProfileLevelId);
}

TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByOffererWithLowLevel)
{
  ForceH264(mSessionOff, 0x42e00b);
  ForceH264(mSessionAns, 0x42e00d);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);

  Replace("level-asymmetry-allowed=1",
          "level-asymmetry-allowed=0",
          &offer);

  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  // Offerer doesn't know about the shenanigans we've pulled here, so will
  // behave normally, and we test the normal behavior elsewhere.

  const JsepCodecDescription* answererSendCodec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec);
  ASSERT_TRUE(answererSendCodec);
  ASSERT_EQ("H264", answererSendCodec->mName);
  const JsepVideoCodecDescription* answererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(answererSendCodec));
  ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId);

  const JsepCodecDescription* answererRecvCodec;
  GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec);
  ASSERT_EQ("H264", answererRecvCodec->mName);
  const JsepVideoCodecDescription* answererVideoRecvCodec(
      static_cast<const JsepVideoCodecDescription*>(answererRecvCodec));
  ASSERT_EQ((uint32_t)0x42e00b, answererVideoRecvCodec->mProfileLevelId);
}

TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByOffererWithHighLevel)
{
  ForceH264(mSessionOff, 0x42e00d);
  ForceH264(mSessionAns, 0x42e00b);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);

  Replace("level-asymmetry-allowed=1",
          "level-asymmetry-allowed=0",
          &offer);

  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  // Offerer doesn't know about the shenanigans we've pulled here, so will
  // behave normally, and we test the normal behavior elsewhere.

  const JsepCodecDescription* answererSendCodec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec);
  ASSERT_TRUE(answererSendCodec);
  ASSERT_EQ("H264", answererSendCodec->mName);
  const JsepVideoCodecDescription* answererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(answererSendCodec));
  ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId);

  const JsepCodecDescription* answererRecvCodec;
  GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec);
  ASSERT_EQ("H264", answererRecvCodec->mName);
  const JsepVideoCodecDescription* answererVideoRecvCodec(
      static_cast<const JsepVideoCodecDescription*>(answererRecvCodec));
  ASSERT_EQ((uint32_t)0x42e00b, answererVideoRecvCodec->mProfileLevelId);
}

TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByAnswererWithLowLevel)
{
  ForceH264(mSessionOff, 0x42e00d);
  ForceH264(mSessionAns, 0x42e00b);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);
  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  Replace("level-asymmetry-allowed=1",
          "level-asymmetry-allowed=0",
          &answer);

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  const JsepCodecDescription* offererSendCodec;
  GetCodec(mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec);
  ASSERT_TRUE(offererSendCodec);
  ASSERT_EQ("H264", offererSendCodec->mName);
  const JsepVideoCodecDescription* offererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(offererSendCodec));
  ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId);

  const JsepCodecDescription* offererRecvCodec;
  GetCodec(mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec);
  ASSERT_EQ("H264", offererRecvCodec->mName);
  const JsepVideoCodecDescription* offererVideoRecvCodec(
      static_cast<const JsepVideoCodecDescription*>(offererRecvCodec));
  ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId);

  // Answerer doesn't know we've pulled these shenanigans, it should act as if
  // it did not set level-asymmetry-required, and we already check that
  // elsewhere
}

TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByAnswererWithHighLevel)
{
  ForceH264(mSessionOff, 0x42e00b);
  ForceH264(mSessionAns, 0x42e00d);

  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer(CreateOffer());
  SetLocalOffer(offer, CHECK_SUCCESS);
  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer(CreateAnswer());

  Replace("level-asymmetry-allowed=1",
          "level-asymmetry-allowed=0",
          &answer);

  SetRemoteAnswer(answer, CHECK_SUCCESS);
  SetLocalAnswer(answer, CHECK_SUCCESS);

  const JsepCodecDescription* offererSendCodec;
  GetCodec(mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec);
  ASSERT_TRUE(offererSendCodec);
  ASSERT_EQ("H264", offererSendCodec->mName);
  const JsepVideoCodecDescription* offererVideoSendCodec(
      static_cast<const JsepVideoCodecDescription*>(offererSendCodec));
  ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId);

  const JsepCodecDescription* offererRecvCodec;
  GetCodec(mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec);
  ASSERT_EQ("H264", offererRecvCodec->mName);
  const JsepVideoCodecDescription* offererVideoRecvCodec(
      static_cast<const JsepVideoCodecDescription*>(offererRecvCodec));
  ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId);

  // Answerer doesn't know we've pulled these shenanigans, it should act as if
  // it did not set level-asymmetry-required, and we already check that
  // elsewhere
}

TEST_P(JsepSessionTest, TestRejectMline)
{
  // We need to do this before adding tracks
  types = BuildTypes(GetParam());
  std::sort(types.begin(), types.end());

  switch (types.front()) {
    case SdpMediaSection::kAudio:
      // Sabotage audio
      EnsureNegotiationFailure(types.front(), "opus");
      break;
    case SdpMediaSection::kVideo:
      // Sabotage video
      EnsureNegotiationFailure(types.front(), "H264");
      break;
    case SdpMediaSection::kApplication:
      // Sabotage datachannel
      EnsureNegotiationFailure(types.front(), "webrtc-datachannel");
      break;
    default:
      ASSERT_TRUE(false) << "Unknown media type";
  }

  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  std::string offer = CreateOffer();
  mSessionOff.SetLocalDescription(kJsepSdpOffer, offer);
  mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer);

  std::string answer = CreateAnswer();

  UniquePtr<Sdp> outputSdp(Parse(answer));
  ASSERT_TRUE(!!outputSdp);

  ASSERT_NE(0U, outputSdp->GetMediaSectionCount());
  SdpMediaSection* failed_section = nullptr;

  for (size_t i = 0; i < outputSdp->GetMediaSectionCount(); ++i) {
    if (outputSdp->GetMediaSection(i).GetMediaType() == types.front()) {
      failed_section = &outputSdp->GetMediaSection(i);
    }
  }

  ASSERT_TRUE(failed_section) << "Failed type was entirely absent from SDP";
  auto& failed_attrs = failed_section->GetAttributeList();
  ASSERT_EQ(SdpDirectionAttribute::kInactive, failed_attrs.GetDirection());
  ASSERT_EQ(0U, failed_section->GetPort());

  mSessionAns.SetLocalDescription(kJsepSdpAnswer, answer);
  mSessionOff.SetRemoteDescription(kJsepSdpAnswer, answer);

  size_t numRejected = std::count(types.begin(), types.end(), types.front());
  size_t numAccepted = types.size() - numRejected;

  ASSERT_EQ(numAccepted, mSessionOff.GetNegotiatedTrackPairs().size());
  ASSERT_EQ(numAccepted, mSessionAns.GetNegotiatedTrackPairs().size());

  ASSERT_EQ(types.size(), mSessionOff.GetTransports().size());
  ASSERT_EQ(types.size(), mSessionOff.GetLocalTracks().size());
  ASSERT_EQ(numAccepted, mSessionOff.GetRemoteTracks().size());

  ASSERT_EQ(types.size(), mSessionAns.GetTransports().size());
  ASSERT_EQ(types.size(), mSessionAns.GetLocalTracks().size());
  ASSERT_EQ(types.size(), mSessionAns.GetRemoteTracks().size());
}

TEST_F(JsepSessionTest, CreateOfferNoMlines)
{
  JsepOfferOptions options;
  std::string offer;
  nsresult rv = mSessionOff.CreateOffer(options, &offer);
  ASSERT_NE(NS_OK, rv);
  ASSERT_NE("", mSessionOff.GetLastError());
}

TEST_F(JsepSessionTest, TestIceLite)
{
  AddTracks(mSessionOff, "audio");
  AddTracks(mSessionAns, "audio");
  std::string offer = CreateOffer();
  SetLocalOffer(offer, CHECK_SUCCESS);

  UniquePtr<Sdp> parsedOffer(Parse(offer));
  parsedOffer->GetAttributeList().SetAttribute(
      new SdpFlagAttribute(SdpAttribute::kIceLiteAttribute));

  std::ostringstream os;
  parsedOffer->Serialize(os);
  SetRemoteOffer(os.str(), CHECK_SUCCESS);

  ASSERT_TRUE(mSessionAns.RemoteIsIceLite());
  ASSERT_FALSE(mSessionOff.RemoteIsIceLite());
}

TEST_F(JsepSessionTest, TestIceOptions)
{
  AddTracks(mSessionOff, "audio");
  AddTracks(mSessionAns, "audio");
  std::string offer = CreateOffer();
  SetLocalOffer(offer, CHECK_SUCCESS);
  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer, CHECK_SUCCESS);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  ASSERT_EQ(1U, mSessionOff.GetIceOptions().size());
  ASSERT_EQ("trickle", mSessionOff.GetIceOptions()[0]);

  ASSERT_EQ(1U, mSessionAns.GetIceOptions().size());
  ASSERT_EQ("trickle", mSessionAns.GetIceOptions()[0]);
}

TEST_F(JsepSessionTest, TestExtmap)
{
  AddTracks(mSessionOff, "audio");
  AddTracks(mSessionAns, "audio");
  // ssrc-audio-level will be extmap 1 for both
  mSessionOff.AddAudioRtpExtension("foo"); // Default mapping of 2
  mSessionOff.AddAudioRtpExtension("bar"); // Default mapping of 3
  mSessionAns.AddAudioRtpExtension("bar"); // Default mapping of 2
  std::string offer = CreateOffer();
  SetLocalOffer(offer, CHECK_SUCCESS);
  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer, CHECK_SUCCESS);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  UniquePtr<Sdp> parsedOffer(Parse(offer));
  ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount());

  auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList();
  ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute));
  auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps;
  ASSERT_EQ(3U, offerExtmap.size());
  ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level",
      offerExtmap[0].extensionname);
  ASSERT_EQ(1U, offerExtmap[0].entry);
  ASSERT_EQ("foo", offerExtmap[1].extensionname);
  ASSERT_EQ(2U, offerExtmap[1].entry);
  ASSERT_EQ("bar", offerExtmap[2].extensionname);
  ASSERT_EQ(3U, offerExtmap[2].entry);

  UniquePtr<Sdp> parsedAnswer(Parse(answer));
  ASSERT_EQ(1U, parsedAnswer->GetMediaSectionCount());

  auto& answerMediaAttrs = parsedAnswer->GetMediaSection(0).GetAttributeList();
  ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute));
  auto& answerExtmap = answerMediaAttrs.GetExtmap().mExtmaps;
  ASSERT_EQ(1U, answerExtmap.size());
  // We ensure that the entry for "bar" matches what was in the offer
  ASSERT_EQ("bar", answerExtmap[0].extensionname);
  ASSERT_EQ(3U, answerExtmap[0].entry);
}

TEST_F(JsepSessionTest, TestRtcpFbStar)
{
  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  std::string offer = CreateOffer();

  UniquePtr<Sdp> parsedOffer(Parse(offer));
  auto* rtcpfbs = new SdpRtcpFbAttributeList;
  rtcpfbs->PushEntry("*", SdpRtcpFbAttributeList::kNack);
  parsedOffer->GetMediaSection(0).GetAttributeList().SetAttribute(rtcpfbs);
  offer = parsedOffer->ToString();

  SetLocalOffer(offer, CHECK_SUCCESS);
  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer, CHECK_SUCCESS);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  ASSERT_EQ(1U, mSessionAns.GetRemoteTracks().size());
  RefPtr<JsepTrack> track = mSessionAns.GetRemoteTracks()[0];
  ASSERT_TRUE(track->GetNegotiatedDetails());
  auto* details = track->GetNegotiatedDetails();
  for (const JsepCodecDescription* codec :
       details->GetEncoding(0).GetCodecs()) {
    const JsepVideoCodecDescription* videoCodec =
      static_cast<const JsepVideoCodecDescription*>(codec);
    ASSERT_EQ(1U, videoCodec->mNackFbTypes.size());
    ASSERT_EQ("", videoCodec->mNackFbTypes[0]);
  }
}

TEST_F(JsepSessionTest, TestUniquePayloadTypes)
{
  // The audio payload types will all appear more than once, but the video
  // payload types will be unique.
  AddTracks(mSessionOff, "audio,audio,video");
  AddTracks(mSessionAns, "audio,audio,video");

  std::string offer = CreateOffer();
  SetLocalOffer(offer, CHECK_SUCCESS);
  SetRemoteOffer(offer, CHECK_SUCCESS);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer, CHECK_SUCCESS);
  SetRemoteAnswer(answer, CHECK_SUCCESS);

  auto offerPairs = mSessionOff.GetNegotiatedTrackPairs();
  auto answerPairs = mSessionAns.GetNegotiatedTrackPairs();
  ASSERT_EQ(3U, offerPairs.size());
  ASSERT_EQ(3U, answerPairs.size());

  ASSERT_TRUE(offerPairs[0].mReceiving);
  ASSERT_TRUE(offerPairs[0].mReceiving->GetNegotiatedDetails());
  ASSERT_EQ(0U,
      offerPairs[0].mReceiving->GetNegotiatedDetails()->
      GetUniquePayloadTypes().size());

  ASSERT_TRUE(offerPairs[1].mReceiving);
  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
  ASSERT_EQ(0U,
      offerPairs[1].mReceiving->GetNegotiatedDetails()->
      GetUniquePayloadTypes().size());

  ASSERT_TRUE(offerPairs[2].mReceiving);
  ASSERT_TRUE(offerPairs[2].mReceiving->GetNegotiatedDetails());
  ASSERT_NE(0U,
      offerPairs[2].mReceiving->GetNegotiatedDetails()->
      GetUniquePayloadTypes().size());

  ASSERT_TRUE(answerPairs[0].mReceiving);
  ASSERT_TRUE(answerPairs[0].mReceiving->GetNegotiatedDetails());
  ASSERT_EQ(0U,
      answerPairs[0].mReceiving->GetNegotiatedDetails()->
      GetUniquePayloadTypes().size());

  ASSERT_TRUE(answerPairs[1].mReceiving);
  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
  ASSERT_EQ(0U,
      answerPairs[1].mReceiving->GetNegotiatedDetails()->
      GetUniquePayloadTypes().size());

  ASSERT_TRUE(answerPairs[2].mReceiving);
  ASSERT_TRUE(answerPairs[2].mReceiving->GetNegotiatedDetails());
  ASSERT_NE(0U,
      answerPairs[2].mReceiving->GetNegotiatedDetails()->
      GetUniquePayloadTypes().size());
}

TEST_F(JsepSessionTest, UnknownFingerprintAlgorithm)
{
  types.push_back(SdpMediaSection::kAudio);
  AddTracks(mSessionOff, "audio");
  AddTracks(mSessionAns, "audio");

  std::string offer(CreateOffer());
  SetLocalOffer(offer);
  ReplaceAll("fingerprint:sha", "fingerprint:foo", &offer);
  nsresult rv = mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer);
  ASSERT_NE(NS_OK, rv);
  ASSERT_NE("", mSessionAns.GetLastError());
}

TEST(H264ProfileLevelIdTest, TestLevelComparisons)
{
  ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x421D0B), // 1b
            JsepVideoCodecDescription::GetSaneH264Level(0x420D0B)); // 1.1
  ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x420D0A), // 1.0
            JsepVideoCodecDescription::GetSaneH264Level(0x421D0B)); // 1b
  ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x420D0A), // 1.0
            JsepVideoCodecDescription::GetSaneH264Level(0x420D0B)); // 1.1

  ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x640009), // 1b
            JsepVideoCodecDescription::GetSaneH264Level(0x64000B)); // 1.1
  ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x64000A), // 1.0
            JsepVideoCodecDescription::GetSaneH264Level(0x640009)); // 1b
  ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x64000A), // 1.0
            JsepVideoCodecDescription::GetSaneH264Level(0x64000B)); // 1.1
}

TEST(H264ProfileLevelIdTest, TestLevelSetting)
{
  uint32_t profileLevelId = 0x420D0A;
  JsepVideoCodecDescription::SetSaneH264Level(
      JsepVideoCodecDescription::GetSaneH264Level(0x42100B),
      &profileLevelId);
  ASSERT_EQ((uint32_t)0x421D0B, profileLevelId);

  JsepVideoCodecDescription::SetSaneH264Level(
      JsepVideoCodecDescription::GetSaneH264Level(0x42000A),
      &profileLevelId);
  ASSERT_EQ((uint32_t)0x420D0A, profileLevelId);

  profileLevelId = 0x6E100A;
  JsepVideoCodecDescription::SetSaneH264Level(
      JsepVideoCodecDescription::GetSaneH264Level(0x640009),
      &profileLevelId);
  ASSERT_EQ((uint32_t)0x6E1009, profileLevelId);

  JsepVideoCodecDescription::SetSaneH264Level(
      JsepVideoCodecDescription::GetSaneH264Level(0x64000B),
      &profileLevelId);
  ASSERT_EQ((uint32_t)0x6E100B, profileLevelId);
}

TEST_F(JsepSessionTest, StronglyPreferredCodec)
{
  for (JsepCodecDescription* codec : mSessionAns.Codecs()) {
    if (codec->mName == "H264") {
      codec->mStronglyPreferred = true;
    }
  }

  types.push_back(SdpMediaSection::kVideo);
  AddTracks(mSessionOff, "video");
  AddTracks(mSessionAns, "video");

  OfferAnswer();

  const JsepCodecDescription* codec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &codec);
  ASSERT_TRUE(codec);
  ASSERT_EQ("H264", codec->mName);
  GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &codec);
  ASSERT_TRUE(codec);
  ASSERT_EQ("H264", codec->mName);
}

TEST_F(JsepSessionTest, LowDynamicPayloadType)
{
  SetPayloadTypeNumber(mSessionOff, "opus", "12");
  types.push_back(SdpMediaSection::kAudio);
  AddTracks(mSessionOff, "audio");
  AddTracks(mSessionAns, "audio");

  OfferAnswer();
  const JsepCodecDescription* codec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &codec);
  ASSERT_TRUE(codec);
  ASSERT_EQ("opus", codec->mName);
  ASSERT_EQ("12", codec->mDefaultPt);
  GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &codec);
  ASSERT_TRUE(codec);
  ASSERT_EQ("opus", codec->mName);
  ASSERT_EQ("12", codec->mDefaultPt);
}

TEST_F(JsepSessionTest, PayloadTypeClash)
{
  // Disable this so mSessionOff doesn't have a duplicate
  SetCodecEnabled(mSessionOff, "PCMU", false);
  SetPayloadTypeNumber(mSessionOff, "opus", "0");
  SetPayloadTypeNumber(mSessionAns, "PCMU", "0");
  types.push_back(SdpMediaSection::kAudio);
  AddTracks(mSessionOff, "audio");
  AddTracks(mSessionAns, "audio");

  OfferAnswer();
  const JsepCodecDescription* codec;
  GetCodec(mSessionAns, 0, sdp::kSend, 0, 0, &codec);
  ASSERT_TRUE(codec);
  ASSERT_EQ("opus", codec->mName);
  ASSERT_EQ("0", codec->mDefaultPt);
  GetCodec(mSessionAns, 0, sdp::kRecv, 0, 0, &codec);
  ASSERT_TRUE(codec);
  ASSERT_EQ("opus", codec->mName);
  ASSERT_EQ("0", codec->mDefaultPt);

  // Now, make sure that mSessionAns does not put a=rtpmap:0 PCMU in a reoffer,
  // since pt 0 is taken for opus (the answerer still supports PCMU, and will
  // reoffer it, but it should choose a new payload type for it)
  JsepOfferOptions options;
  std::string reoffer;
  nsresult rv = mSessionAns.CreateOffer(options, &reoffer);
  ASSERT_EQ(NS_OK, rv);
  ASSERT_EQ(std::string::npos, reoffer.find("a=rtpmap:0 PCMU")) << reoffer;
}

TEST_P(JsepSessionTest, TestGlareRollback)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);
  JsepOfferOptions options;

  std::string offer;
  ASSERT_EQ(NS_OK, mSessionAns.CreateOffer(options, &offer));
  ASSERT_EQ(NS_OK,
            mSessionAns.SetLocalDescription(kJsepSdpOffer, offer));
  ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionAns.GetState());

  ASSERT_EQ(NS_OK, mSessionOff.CreateOffer(options, &offer));
  ASSERT_EQ(NS_OK,
            mSessionOff.SetLocalDescription(kJsepSdpOffer, offer));
  ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff.GetState());

  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer));
  ASSERT_EQ(NS_OK,
            mSessionAns.SetLocalDescription(kJsepSdpRollback, ""));
  ASSERT_EQ(kJsepStateStable, mSessionAns.GetState());

  SetRemoteOffer(offer);

  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);
}

TEST_P(JsepSessionTest, TestRejectOfferRollback)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  std::string offer = CreateOffer();
  SetLocalOffer(offer);
  SetRemoteOffer(offer);

  ASSERT_EQ(NS_OK,
            mSessionAns.SetRemoteDescription(kJsepSdpRollback, ""));
  ASSERT_EQ(kJsepStateStable, mSessionAns.GetState());
  ASSERT_EQ(types.size(), mSessionAns.GetRemoteTracksRemoved().size());

  ASSERT_EQ(NS_OK,
            mSessionOff.SetLocalDescription(kJsepSdpRollback, ""));
  ASSERT_EQ(kJsepStateStable, mSessionOff.GetState());

  OfferAnswer();
}

TEST_P(JsepSessionTest, TestInvalidRollback)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionOff.SetLocalDescription(kJsepSdpRollback, ""));
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionOff.SetRemoteDescription(kJsepSdpRollback, ""));

  std::string offer = CreateOffer();
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionOff.SetLocalDescription(kJsepSdpRollback, ""));
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionOff.SetRemoteDescription(kJsepSdpRollback, ""));

  SetLocalOffer(offer);
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionOff.SetRemoteDescription(kJsepSdpRollback, ""));

  SetRemoteOffer(offer);
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionAns.SetLocalDescription(kJsepSdpRollback, ""));

  std::string answer = CreateAnswer();
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionAns.SetLocalDescription(kJsepSdpRollback, ""));

  SetLocalAnswer(answer);
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionAns.SetLocalDescription(kJsepSdpRollback, ""));
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionAns.SetRemoteDescription(kJsepSdpRollback, ""));

  SetRemoteAnswer(answer);
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionOff.SetLocalDescription(kJsepSdpRollback, ""));
  ASSERT_EQ(NS_ERROR_UNEXPECTED,
            mSessionOff.SetRemoteDescription(kJsepSdpRollback, ""));
}

size_t GetActiveTransportCount(const JsepSession& session)
{
  auto transports = session.GetTransports();
  size_t activeTransportCount = 0;
  for (RefPtr<JsepTransport>& transport : transports) {
    activeTransportCount += transport->mComponents;
  }
  return activeTransportCount;
}

TEST_P(JsepSessionTest, TestBalancedBundle)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  mSessionOff.SetBundlePolicy(kBundleBalanced);

  std::string offer = CreateOffer();
  SipccSdpParser parser;
  UniquePtr<Sdp> parsedOffer = parser.Parse(offer);
  ASSERT_TRUE(parsedOffer.get());

  std::map<SdpMediaSection::MediaType, SdpMediaSection*> firstByType;

  for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) {
    SdpMediaSection& msection(parsedOffer->GetMediaSection(i));
    bool firstOfType = !firstByType.count(msection.GetMediaType());
    if (firstOfType) {
      firstByType[msection.GetMediaType()] = &msection;
    }
    ASSERT_EQ(!firstOfType,
              msection.GetAttributeList().HasAttribute(
                SdpAttribute::kBundleOnlyAttribute));
  }

  SetLocalOffer(offer);
  SetRemoteOffer(offer);
  std::string answer = CreateAnswer();
  SetLocalAnswer(answer);
  SetRemoteAnswer(answer);

  CheckPairs(mSessionOff, "Offerer pairs");
  CheckPairs(mSessionAns, "Answerer pairs");
  EXPECT_EQ(1U, GetActiveTransportCount(mSessionOff));
  EXPECT_EQ(1U, GetActiveTransportCount(mSessionAns));
}

TEST_P(JsepSessionTest, TestMaxBundle)
{
  AddTracks(mSessionOff);
  AddTracks(mSessionAns);

  mSessionOff.SetBundlePolicy(kBundleMaxBundle);
  OfferAnswer();

  std::string offer = mSessionOff.GetLocalDescription();
  SipccSdpParser parser;
  UniquePtr<Sdp> parsedOffer = parser.Parse(offer);
  ASSERT_TRUE(parsedOffer.get());

  ASSERT_FALSE(
      parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute(
        SdpAttribute::kBundleOnlyAttribute));
  for (size_t i = 1; i < parsedOffer->GetMediaSectionCount(); ++i) {
    ASSERT_TRUE(
        parsedOffer->GetMediaSection(i).GetAttributeList().HasAttribute(
          SdpAttribute::kBundleOnlyAttribute));
  }


  CheckPairs(mSessionOff, "Offerer pairs");
  CheckPairs(mSessionAns, "Answerer pairs");
  EXPECT_EQ(1U, GetActiveTransportCount(mSessionOff));
  EXPECT_EQ(1U, GetActiveTransportCount(mSessionAns));
}

TEST_F(JsepSessionTest, TestNonDefaultProtocol)
{
  AddTracks(mSessionOff, "audio,video,datachannel");
  AddTracks(mSessionAns, "audio,video,datachannel");

  std::string offer;
  ASSERT_EQ(NS_OK, mSessionOff.CreateOffer(JsepOfferOptions(), &offer));
  offer.replace(offer.find("UDP/TLS/RTP/SAVPF"),
                strlen("UDP/TLS/RTP/SAVPF"),
                "RTP/SAVPF");
  offer.replace(offer.find("UDP/TLS/RTP/SAVPF"),
                strlen("UDP/TLS/RTP/SAVPF"),
                "RTP/SAVPF");
  mSessionOff.SetLocalDescription(kJsepSdpOffer, offer);
  mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer);

  std::string answer;
  mSessionAns.CreateAnswer(JsepAnswerOptions(), &answer);
  UniquePtr<Sdp> parsedAnswer = Parse(answer);
  ASSERT_EQ(3U, parsedAnswer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kRtpSavpf,
            parsedAnswer->GetMediaSection(0).GetProtocol());
  ASSERT_EQ(SdpMediaSection::kRtpSavpf,
            parsedAnswer->GetMediaSection(1).GetProtocol());

  mSessionAns.SetLocalDescription(kJsepSdpAnswer, answer);
  mSessionOff.SetRemoteDescription(kJsepSdpAnswer, answer);

  // Make sure reoffer uses the same protocol as before
  mSessionOff.CreateOffer(JsepOfferOptions(), &offer);
  UniquePtr<Sdp> parsedOffer = Parse(offer);
  ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kRtpSavpf,
            parsedOffer->GetMediaSection(0).GetProtocol());
  ASSERT_EQ(SdpMediaSection::kRtpSavpf,
            parsedOffer->GetMediaSection(1).GetProtocol());

  // Make sure reoffer from other side uses the same protocol as before
  mSessionAns.CreateOffer(JsepOfferOptions(), &offer);
  parsedOffer = Parse(offer);
  ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount());
  ASSERT_EQ(SdpMediaSection::kRtpSavpf,
            parsedOffer->GetMediaSection(0).GetProtocol());
  ASSERT_EQ(SdpMediaSection::kRtpSavpf,
            parsedOffer->GetMediaSection(1).GetProtocol());
}

} // namespace mozilla

int
main(int argc, char** argv)
{
  // Prevents some log spew
  ScopedXPCOM xpcom("jsep_session_unittest");

  NSS_NoDB_Init(nullptr);
  NSS_SetDomesticPolicy();

  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
