/* 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 "nss.h"
#include "ssl.h"

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

#include "CodecConfig.h"
#include "MockJsepCodecPreferences.h"
#include "jsapi/DefaultCodecPreferences.h"
#include "jsapi/RTCRtpTransceiver.h"
#include "jsep/JsepTrack.h"
#include "sdp/SipccSdp.h"
#include "sdp/SipccSdpParser.h"
#include "sdp/SdpHelper.h"

#include <tuple>

using testing::UnorderedElementsAre;

namespace mozilla {

class JsepTrackTestBase : public ::testing::Test {
 public:
  static void SetUpTestCase() {
    NSS_NoDB_Init(nullptr);
    NSS_SetDomesticPolicy();
  }
};

struct CodecOverrides {
  bool addFecCodecs = false;
  bool preferRed = false;
  bool addDtmfCodec = false;
  bool enableRemb = true;
  bool enableTransportCC = true;
  bool enableAudioTransportCC = true;
  bool enableRtx = true;
  void ApplyToPrefs(MockJsepCodecPreferences& aPrefs) const {
    aPrefs.mUseRemb = enableRemb;
    aPrefs.mUseTransportCC = enableTransportCC;
    aPrefs.mUseAudioTransportCC = enableAudioTransportCC;
    aPrefs.mUseRtx = enableRtx;
  }
};

class JsepTrackTest : public JsepTrackTestBase {
 public:
  JsepTrackTest()
      : mSendOff(SdpMediaSection::kAudio, sdp::kSend),
        mRecvOff(SdpMediaSection::kAudio, sdp::kRecv),
        mSendAns(SdpMediaSection::kAudio, sdp::kSend),
        mRecvAns(SdpMediaSection::kAudio, sdp::kRecv) {}

  void TearDown() override {
    if (::testing::UnitTest::GetInstance()
            ->current_test_info()
            ->result()
            ->Failed()) {
      if (mOffer) {
        std::cerr << "Offer SDP: \n";
        mOffer->Serialize(std::cerr);
      }

      if (mAnswer) {
        std::cerr << "Answer SDP: \n";
        mAnswer->Serialize(std::cerr);
      }
    }
  }

  std::vector<UniquePtr<JsepCodecDescription>> MakeCodecs(
      const CodecOverrides overrides) const {
    MockJsepCodecPreferences prefs;
    overrides.ApplyToPrefs(prefs);

    prefs.mUseRemb = overrides.enableRemb;
    prefs.mUseTransportCC = overrides.enableTransportCC;
    JsepCodecPreferences& prefsRef = prefs;
    std::cout << "CodecPrefrences: " << prefsRef << "\n";
    std::vector<UniquePtr<JsepCodecDescription>> results;
    results.emplace_back(JsepAudioCodecDescription::CreateDefaultOpus(prefs));
    results.emplace_back(JsepAudioCodecDescription::CreateDefaultG722(prefs));
    if (overrides.addDtmfCodec) {
      results.emplace_back(
          JsepAudioCodecDescription::CreateDefaultTelephoneEvent());
    }

    if (overrides.addFecCodecs && overrides.preferRed) {
      results.emplace_back(JsepVideoCodecDescription::CreateDefaultRed(prefs));
    }
    results.emplace_back(JsepVideoCodecDescription::CreateDefaultVP8(prefs));
    results.emplace_back(JsepVideoCodecDescription::CreateDefaultH264_1(prefs));
    results.emplace_back(JsepVideoCodecDescription::CreateDefaultAV1(prefs));

    if (overrides.addFecCodecs) {
      if (!overrides.preferRed) {
        results.emplace_back(
            JsepVideoCodecDescription::CreateDefaultRed(prefs));
      }
      results.emplace_back(
          JsepVideoCodecDescription::CreateDefaultUlpFec(prefs));
    }

    results.emplace_back(new JsepApplicationCodecDescription(
        "webrtc-datachannel", 256, 5999, 499));

    return results;
  }

  void Init(SdpMediaSection::MediaType type) {
    InitCodecs(CodecOverrides{});
    InitTracks(type);
    InitSdp(type);
  }

  struct SplitOverrides {
    CodecOverrides offer = {};
    CodecOverrides answer = {};
  };

  void InitCodecs(const CodecOverrides& overrides) {
    mOffCodecs = MakeCodecs(overrides);
    mAnsCodecs = MakeCodecs(overrides);
  }
  void InitCodecs(const SplitOverrides& overrides) {
    mOffCodecs = MakeCodecs(overrides.offer);
    mAnsCodecs = MakeCodecs(overrides.answer);
  }

  void InitTracks(SdpMediaSection::MediaType type) {
    mSendOff = JsepTrack(type, sdp::kSend);
    if (type != SdpMediaSection::MediaType::kApplication) {
      mSendOff.UpdateStreamIds(std::vector<std::string>(1, "stream_id"));
    }
    mRecvOff = JsepTrack(type, sdp::kRecv);
    mSendOff.PopulateCodecs(mOffCodecs);
    mRecvOff.PopulateCodecs(mOffCodecs);

    mSendAns = JsepTrack(type, sdp::kSend);
    if (type != SdpMediaSection::MediaType::kApplication) {
      mSendAns.UpdateStreamIds(std::vector<std::string>(1, "stream_id"));
    }
    mRecvAns = JsepTrack(type, sdp::kRecv);
    mSendAns.PopulateCodecs(mAnsCodecs);
    mRecvAns.PopulateCodecs(mAnsCodecs);
  }

  void InitSdp(SdpMediaSection::MediaType type) {
    std::vector<std::string> msids(1, "*");
    std::string error;
    SdpHelper helper(&error);

    mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
    mOffer->AddMediaSection(type, SdpDirectionAttribute::kSendrecv, 0,
                            SdpHelper::GetProtocolForMediaType(type),
                            sdp::kIPv4, "0.0.0.0");
    // JsepTrack doesn't set msid-semantic
    helper.SetupMsidSemantic(msids, mOffer.get());

    mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
    mAnswer->AddMediaSection(type, SdpDirectionAttribute::kSendrecv, 0,
                             SdpHelper::GetProtocolForMediaType(type),
                             sdp::kIPv4, "0.0.0.0");
    // JsepTrack doesn't set msid-semantic
    helper.SetupMsidSemantic(msids, mAnswer.get());
  }

  SdpMediaSection& GetOffer() { return mOffer->GetMediaSection(0); }

  SdpMediaSection& GetAnswer() { return mAnswer->GetMediaSection(0); }

  void CreateOffer() {
    mSendOff.AddToOffer(mSsrcGenerator, &GetOffer());
    mRecvOff.AddToOffer(mSsrcGenerator, &GetOffer());
  }

  void CreateAnswer() {
    if (mRecvAns.GetMediaType() != SdpMediaSection::MediaType::kApplication) {
      mRecvAns.RecvTrackSetRemote(*mOffer, GetOffer());
      mSendAns.SendTrackSetRemote(mSsrcGenerator, GetOffer());
    }

    mSendAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer());
    mRecvAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer());
  }

  void Negotiate() {
    if (mRecvOff.GetMediaType() != SdpMediaSection::MediaType::kApplication) {
      mRecvOff.RecvTrackSetRemote(*mAnswer, GetAnswer());
      mSendOff.SendTrackSetRemote(mSsrcGenerator, GetAnswer());
    }

    if (GetAnswer().IsSending()) {
      mSendAns.Negotiate(GetAnswer(), GetOffer(), GetAnswer());
      mRecvOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer());
    }

    if (GetAnswer().IsReceiving()) {
      mRecvAns.Negotiate(GetAnswer(), GetOffer(), GetAnswer());
      mSendOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer());
    }
  }

  void OfferAnswer(bool offerCodecsMatchAnswer = true) {
    CreateOffer();
    CreateAnswer();
    Negotiate();
    SanityCheck(offerCodecsMatchAnswer);
  }

  // TODO: Look into writing a macro that wraps an ASSERT_ and returns false
  // if it fails (probably requires writing a bool-returning function that
  // takes a void-returning lambda with a bool outparam, which will in turn
  // invokes the ASSERT_)
  static void CheckEncodingCount(size_t expected, const JsepTrack& send,
                                 const JsepTrack& recv) {
    if (expected) {
      ASSERT_TRUE(send.GetNegotiatedDetails());
      ASSERT_TRUE(recv.GetNegotiatedDetails());
    }

    if (!send.GetStreamIds().empty() && send.GetNegotiatedDetails()) {
      ASSERT_EQ(expected, send.GetNegotiatedDetails()->GetEncodingCount());
    }

    if (!recv.GetStreamIds().empty() && recv.GetNegotiatedDetails()) {
      ASSERT_EQ(expected, recv.GetNegotiatedDetails()->GetEncodingCount());
    }
  }

  void CheckOffEncodingCount(size_t expected) const {
    CheckEncodingCount(expected, mSendOff, mRecvAns);
  }

  void CheckAnsEncodingCount(size_t expected) const {
    CheckEncodingCount(expected, mSendAns, mRecvOff);
  }

  UniquePtr<JsepCodecDescription> GetCodec(const JsepTrack& track,
                                           SdpMediaSection::MediaType type,
                                           size_t expectedSize,
                                           size_t codecIndex) const {
    if (!track.GetNegotiatedDetails() ||
        track.GetNegotiatedDetails()->GetEncodingCount() != 1U ||
        track.GetMediaType() != type) {
      return nullptr;
    }
    const auto& codecs =
        track.GetNegotiatedDetails()->GetEncoding(0).GetCodecs();
    // it should not be possible for codecs to have a different type
    // than the track, but we'll check the codec here just in case.
    if (codecs.size() != expectedSize || codecIndex >= expectedSize ||
        codecs[codecIndex]->Type() != type) {
      return nullptr;
    }
    return UniquePtr<JsepCodecDescription>(codecs[codecIndex]->Clone());
  }

  UniquePtr<JsepCodecDescription> GetCodec(const JsepTrack& track,
                                           const std::string& name) const {
    if (!track.GetNegotiatedDetails() ||
        track.GetNegotiatedDetails()->GetEncodingCount() != 1U) {
      return nullptr;
    }

    const auto& codecs =
        track.GetNegotiatedDetails()->GetEncoding(0).GetCodecs();
    for (const auto& codec : codecs) {
      if (codec->mName == name) {
        return UniquePtr<JsepCodecDescription>(codec->Clone());
      }
    }
    return nullptr;
  }

  UniquePtr<JsepAudioCodecDescription> AsAudio(
      UniquePtr<JsepCodecDescription> codec) {
    if (codec && codec->Type() == SdpMediaSection::kAudio) {
      return UniquePtr<JsepAudioCodecDescription>(
          static_cast<JsepAudioCodecDescription*>(codec.release()));
    }
    return nullptr;
  }

  UniquePtr<JsepVideoCodecDescription> AsVideo(
      UniquePtr<JsepCodecDescription> codec) {
    if (codec && codec->Type() == SdpMediaSection::kVideo) {
      return UniquePtr<JsepVideoCodecDescription>(
          static_cast<JsepVideoCodecDescription*>(codec.release()));
    }
    return nullptr;
  }

  UniquePtr<JsepVideoCodecDescription> GetVideoCodec(
      const JsepTrack& track, size_t expectedSize = 1,
      size_t codecIndex = 0) const {
    auto codec =
        GetCodec(track, SdpMediaSection::kVideo, expectedSize, codecIndex);
    return UniquePtr<JsepVideoCodecDescription>(
        static_cast<JsepVideoCodecDescription*>(codec.release()));
  }

  UniquePtr<JsepAudioCodecDescription> GetAudioCodec(
      const JsepTrack& track, size_t expectedSize = 1,
      size_t codecIndex = 0) const {
    auto codec =
        GetCodec(track, SdpMediaSection::kAudio, expectedSize, codecIndex);
    return UniquePtr<JsepAudioCodecDescription>(
        static_cast<JsepAudioCodecDescription*>(codec.release()));
  }

  template <typename T>
  void CheckOtherFbExists(const T& codec,
                          SdpRtcpFbAttributeList::Type type) const {
    for (const auto& fb : codec.mOtherFbTypes) {
      if (fb.type == type) {
        return;  // found the RtcpFb type, so stop looking
      }
    }
    FAIL();  // RtcpFb type not found
  }

  void SanityCheckRtcpFbs(const JsepVideoCodecDescription& a,
                          const JsepVideoCodecDescription& b) const {
    ASSERT_EQ(a.mNackFbTypes.size(), b.mNackFbTypes.size());
    ASSERT_EQ(a.mAckFbTypes.size(), b.mAckFbTypes.size());
    ASSERT_EQ(a.mCcmFbTypes.size(), b.mCcmFbTypes.size());
    ASSERT_EQ(a.mOtherFbTypes.size(), b.mOtherFbTypes.size());
  }

  void SanityCheckCodecs(const JsepCodecDescription& a,
                         const JsepCodecDescription& b) const {
#define MSG                                                               \
  "For codecs " << a.mName << " (" << a.mDirection << ") and " << b.mName \
                << " (" << b.mDirection << ")"
    ASSERT_EQ(a.Type(), b.Type()) << MSG;
    if (a.Type() != SdpMediaSection::kApplication) {
      ASSERT_EQ(a.mDefaultPt, b.mDefaultPt) << MSG;
    }
    ASSERT_EQ(a.mName, b.mName);
    if (!mExpectDifferingFmtp) {
      ASSERT_EQ(a.mSdpFmtpLine, b.mSdpFmtpLine) << MSG;
    }
    ASSERT_EQ(a.mClock, b.mClock) << MSG;
    ASSERT_EQ(a.mChannels, b.mChannels) << MSG;
    ASSERT_NE(a.mDirection, b.mDirection) << MSG;
    // These constraints are for fmtp and rid, which _are_ signaled
    ASSERT_EQ(a.mConstraints, b.mConstraints) << MSG;
#undef MSG

    if (a.Type() == SdpMediaSection::kVideo) {
      SanityCheckRtcpFbs(static_cast<const JsepVideoCodecDescription&>(a),
                         static_cast<const JsepVideoCodecDescription&>(b));
    }
  }

  void SanityCheckEncodings(const JsepTrackEncoding& a,
                            const JsepTrackEncoding& b) const {
    ASSERT_EQ(a.GetCodecs().size(), b.GetCodecs().size());
    for (size_t i = 0; i < a.GetCodecs().size(); ++i) {
      SanityCheckCodecs(*a.GetCodecs()[i], *b.GetCodecs()[i]);
    }

    ASSERT_EQ(a.mRid, b.mRid);
    // mConstraints will probably differ, since they are not signaled to the
    // other side.
  }

  void SanityCheckNegotiatedDetails(const JsepTrack& aTrack,
                                    const JsepTrack& bTrack,
                                    bool codecsMustMatch) const {
    const auto aDetails = *aTrack.GetNegotiatedDetails();
    const auto bDetails = *bTrack.GetNegotiatedDetails();
    ASSERT_EQ(aDetails.GetEncodingCount(), bDetails.GetEncodingCount());
    if (codecsMustMatch) {
      for (size_t i = 0; i < aDetails.GetEncodingCount(); ++i) {
        SanityCheckEncodings(aDetails.GetEncoding(i), bDetails.GetEncoding(i));
      }
    }

    ASSERT_EQ(aTrack.GetUniqueReceivePayloadTypes().size(),
              bTrack.GetUniqueReceivePayloadTypes().size());
    for (size_t i = 0; i < aTrack.GetUniqueReceivePayloadTypes().size(); ++i) {
      ASSERT_EQ(aTrack.GetUniqueReceivePayloadTypes()[i],
                bTrack.GetUniqueReceivePayloadTypes()[i]);
    }
  }

  void SanityCheckTracks(const JsepTrack& a, const JsepTrack& b,
                         bool codecsMustMatch) const {
    if (!a.GetNegotiatedDetails()) {
      ASSERT_FALSE(!!b.GetNegotiatedDetails());
      return;
    }

    ASSERT_TRUE(!!a.GetNegotiatedDetails());
    ASSERT_TRUE(!!b.GetNegotiatedDetails());
    ASSERT_EQ(a.GetMediaType(), b.GetMediaType());
    ASSERT_EQ(a.GetStreamIds(), b.GetStreamIds());
    ASSERT_EQ(a.GetCNAME(), b.GetCNAME());
    ASSERT_NE(a.GetDirection(), b.GetDirection());
    ASSERT_EQ(a.GetSsrcs().size(), b.GetSsrcs().size());
    for (size_t i = 0; i < a.GetSsrcs().size(); ++i) {
      ASSERT_EQ(a.GetSsrcs()[i], b.GetSsrcs()[i]);
    }

    SanityCheckNegotiatedDetails(a, b, codecsMustMatch);
  }

  void SanityCheck(bool offerCodecsMatchAnswer = true) const {
    SanityCheckTracks(mSendOff, mRecvAns, true);
    SanityCheckTracks(mRecvOff, mSendAns, offerCodecsMatchAnswer);
  }

 protected:
  JsepTrack mSendOff;
  JsepTrack mRecvOff;
  JsepTrack mSendAns;
  JsepTrack mRecvAns;
  std::vector<UniquePtr<JsepCodecDescription>> mOffCodecs;
  std::vector<UniquePtr<JsepCodecDescription>> mAnsCodecs;
  UniquePtr<Sdp> mOffer;
  UniquePtr<Sdp> mAnswer;
  SsrcGenerator mSsrcGenerator;
  bool mExpectDifferingFmtp = false;
};

TEST_F(JsepTrackTestBase, CreateDestroy) {}

TEST_F(JsepTrackTest, CreateDestroy) { Init(SdpMediaSection::kAudio); }

TEST_F(JsepTrackTest, AudioNegotiation) {
  Init(SdpMediaSection::kAudio);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);
}

TEST_F(JsepTrackTest, VideoNegotiation) {
  Init(SdpMediaSection::kVideo);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);
}

class CheckForCodecType {
 public:
  explicit CheckForCodecType(SdpMediaSection::MediaType type, bool* result)
      : mResult(result), mType(type) {}

  void operator()(const UniquePtr<JsepCodecDescription>& codec) {
    if (codec->Type() == mType) {
      *mResult = true;
    }
  }

 private:
  bool* mResult;
  SdpMediaSection::MediaType mType;
};

TEST_F(JsepTrackTest, CheckForAnsweringWithExtmapAllowMixedWhenNotOffered) {
  Init(SdpMediaSection::kAudio);

  CreateOffer();
  mOffer->GetMediaSection(0).GetAttributeList().RemoveAttribute(
      SdpAttribute::kExtmapAllowMixedAttribute);
  CreateAnswer();

  ASSERT_FALSE(mAnswer->GetMediaSection(0).GetAttributeList().HasAttribute(
      SdpAttribute::kExtmapAllowMixedAttribute));
  Negotiate();
  SanityCheck();
}

TEST_F(JsepTrackTest, CheckForMismatchedAudioCodecAndVideoTrack) {
  std::vector<UniquePtr<JsepCodecDescription>> offerCodecs;

  // make codecs including telephone-event (an audio codec)
  offerCodecs = MakeCodecs({.addDtmfCodec = true});
  JsepTrack videoTrack(SdpMediaSection::kVideo, sdp::kSend);
  videoTrack.UpdateStreamIds(std::vector<std::string>(1, "stream_id"));
  // populate codecs and then make sure we don't have any audio codecs
  // in the video track
  videoTrack.PopulateCodecs(offerCodecs);

  bool found = false;
  videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
  ASSERT_FALSE(found);

  found = false;
  videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kVideo, &found));
  ASSERT_TRUE(found);  // for sanity, make sure we did find video codecs
}

TEST_F(JsepTrackTest, CheckVideoTrackWithHackedDtmfSdp) {
  Init(SdpMediaSection::kVideo);
  CreateOffer();
  // make sure we don't find sdp containing telephone-event in video track
  ASSERT_EQ(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  // force audio codec telephone-event into video m= section of offer
  GetOffer().AddCodec("101", "telephone-event", 8000, 1);
  // make sure we _do_ find sdp containing telephone-event in video track
  ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  CreateAnswer();
  // make sure we don't find sdp containing telephone-event in video track
  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  // force audio codec telephone-event into video m= section of answer
  GetAnswer().AddCodec("101", "telephone-event", 8000, 1);
  // make sure we _do_ find sdp containing telephone-event in video track
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  Negotiate();
  SanityCheck();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  // make sure we still don't find any audio codecs in the video track after
  // hacking the sdp
  bool found = false;
  mSendOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
  ASSERT_FALSE(found);
  mRecvOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
  ASSERT_FALSE(found);
  mSendAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
  ASSERT_FALSE(found);
  mRecvAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
  ASSERT_FALSE(found);
}

TEST_F(JsepTrackTest, AudioNegotiationOffererDtmf) {
  InitCodecs(
      {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = false}});

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer(false);

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);

  UniquePtr<JsepAudioCodecDescription> track;
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
}

TEST_F(JsepTrackTest, AudioNegotiationAnswererDtmf) {
  InitCodecs(
      {.offer = {.addDtmfCodec = false}, .answer = {.addDtmfCodec = true}});

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_EQ(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  ASSERT_EQ(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);

  UniquePtr<JsepAudioCodecDescription> track;
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
}

TEST_F(JsepTrackTest, AudioNegotiationOffererAnswererDtmf) {
  InitCodecs(
      {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}});

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos);

  UniquePtr<JsepAudioCodecDescription> track;
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
}

TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererFmtp) {
  InitCodecs(
      {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}});

  mExpectDifferingFmtp = true;

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);

  CreateOffer();
  GetOffer().RemoveFmtp("101");

  CreateAnswer();

  Negotiate();
  SanityCheck();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos);

  UniquePtr<JsepAudioCodecDescription> track;
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing"));
}

TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererFmtpAnswererNoFmtp) {
  InitCodecs(
      {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}});

  mExpectDifferingFmtp = true;

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);

  CreateOffer();

  CreateAnswer();
  GetAnswer().RemoveFmtp("101");

  Negotiate();
  SanityCheck();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);

  UniquePtr<JsepAudioCodecDescription> track;
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
}

TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererNoFmtp) {
  InitCodecs(
      {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}});

  mExpectDifferingFmtp = true;

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);

  CreateOffer();
  GetOffer().RemoveFmtp("101");

  CreateAnswer();
  GetAnswer().RemoveFmtp("101");

  Negotiate();
  SanityCheck();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
            std::string::npos);

  ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);

  UniquePtr<JsepAudioCodecDescription> track;
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0)));
  ASSERT_EQ("109", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1)));
  ASSERT_EQ("9", track->mDefaultPt);
  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2)));
  ASSERT_EQ("101", track->mDefaultPt);
  ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing"));
}

TEST_F(JsepTrackTest, VideoNegotationOffererFEC) {
  InitCodecs(
      {.offer = {.addFecCodecs = true}, .answer = {.addFecCodecs = false}});

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer(false);

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);

  UniquePtr<JsepVideoCodecDescription> track;
  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 5, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 5, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
}

TEST_F(JsepTrackTest, VideoNegotationAnswererFEC) {
  InitCodecs(
      {.offer = {.addFecCodecs = false}, .answer = {.addFecCodecs = true}});

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_EQ(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_EQ(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);

  UniquePtr<JsepVideoCodecDescription> track;
  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 3, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3, 1)));
  ASSERT_EQ("126", track->mDefaultPt);
}

TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFEC) {
  InitCodecs(
      {.offer = {.addFecCodecs = true}, .answer = {.addFecCodecs = true}});

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);

  UniquePtr<JsepVideoCodecDescription> track;
  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
}

TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECPreferred) {
  InitCodecs({.offer = {.addFecCodecs = true, .preferRed = true},
              .answer = {.addFecCodecs = true}});

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);

  UniquePtr<JsepVideoCodecDescription> track;
  // We should have 4 codecs, the first of which is VP8, because having a
  // pseudo codec come first is silly.
  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
}

// Make sure we only put the right things in the fmtp:122 120/.... line
TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECMismatch) {
  InitCodecs({.offer = {.addFecCodecs = true, .preferRed = true},
              .answer = {.addFecCodecs = true}});
  // remove h264 & AV1 from answer codecs
  ASSERT_EQ("H264", mAnsCodecs[3]->mName);
  ASSERT_EQ("AV1", mAnsCodecs[4]->mName);
  mAnsCodecs.erase(mAnsCodecs.begin() + 4);
  mAnsCodecs.erase(mAnsCodecs.begin() + 3);

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer(false);

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);

  // We should have 3 codecs, the first of which is VP8, because having a
  // pseudo codec come first is silly.
  UniquePtr<JsepVideoCodecDescription> track;
  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 5)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3)));
  ASSERT_EQ("120", track->mDefaultPt);
  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3)));
  ASSERT_EQ("120", track->mDefaultPt);
}

TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECZeroVP9Codec) {
  MockJsepCodecPreferences prefs;
  mOffCodecs = MakeCodecs({.addFecCodecs = true});
  auto vp9 = JsepVideoCodecDescription::CreateDefaultVP9(prefs);
  vp9->mDefaultPt = "0";
  mOffCodecs.push_back(std::move(vp9));

  ASSERT_EQ(9U, mOffCodecs.size());
  JsepVideoCodecDescription& red =
      static_cast<JsepVideoCodecDescription&>(*mOffCodecs[5]);
  ASSERT_EQ("red", red.mName);

  mAnsCodecs = MakeCodecs({.addFecCodecs = true});

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
}

TEST_F(JsepTrackTest, VideoNegotiationOfferRemb) {
  // enable remb on the offer codecs
  InitCodecs({.offer = {.enableRemb = true, .enableTransportCC = false},
              .answer = {.enableRemb = false, .enableTransportCC = false}});
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // make sure REMB is on offer and not on answer
  ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepVideoCodecDescription> codec;
  ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);

  ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
}

TEST_F(JsepTrackTest, VideoNegotiationAnswerRemb) {
  InitCodecs({.offer = {.enableRemb = false, .enableTransportCC = false},
              .answer = {.enableRemb = true, .enableTransportCC = false}});
  // enable remb on the answer codecs
  ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableRemb();
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // make sure REMB is not on offer and not on answer
  ASSERT_EQ(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepVideoCodecDescription> codec;
  ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
}

TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerRemb) {
  InitCodecs({.offer = {.enableRemb = true, .enableTransportCC = false},
              .answer = {.enableRemb = true, .enableTransportCC = false}});
  // enable remb on the offer and answer codecs
  ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableRemb();
  ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableRemb();
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // make sure REMB is on offer and on answer
  ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
            std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepVideoCodecDescription> codec;
  ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb);
  ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb);
}

TEST_F(JsepTrackTest, AudioNegotiationOfferTransportCC) {
  InitCodecs({.enableAudioTransportCC = false});
  // enable TransportCC on the offer codecs
  ((JsepAudioCodecDescription&)*mOffCodecs[0]).EnableTransportCC();
  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  // make sure TransportCC is on offer and not on answer
  ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:109 transport-cc"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:109 transport-cc"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepAudioCodecDescription> codec;
  ASSERT_TRUE((codec = GetAudioCodec(mSendOff, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetAudioCodec(mRecvAns, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetAudioCodec(mSendAns, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetAudioCodec(mRecvOff, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
}

TEST_F(JsepTrackTest, AudioNegotiationAnswerTransportCC) {
  InitCodecs({.enableAudioTransportCC = false});
  // enable TransportCC on the answer codecs
  ((JsepAudioCodecDescription&)*mAnsCodecs[0]).EnableTransportCC();
  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  // make sure TransportCC is not on offer and not on answer
  ASSERT_EQ(mOffer->ToString().find("a=rtcp-fb:109 transport-cc"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:109 transport-cc"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepAudioCodecDescription> codec;
  ASSERT_TRUE((codec = GetAudioCodec(mSendOff, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetAudioCodec(mRecvAns, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetAudioCodec(mSendAns, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetAudioCodec(mRecvOff, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
}

TEST_F(JsepTrackTest, AudioNegotiationOfferAnswerTransportCC) {
  InitCodecs({.enableAudioTransportCC = false});
  // enable TransportCC on the offer and answer codecs
  ((JsepAudioCodecDescription&)*mOffCodecs[0]).EnableTransportCC();
  ((JsepAudioCodecDescription&)*mAnsCodecs[0]).EnableTransportCC();
  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  // make sure TransportCC is on offer and on answer
  ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:109 transport-cc"),
            std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtcp-fb:109 transport-cc"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepAudioCodecDescription> codec;
  ASSERT_TRUE((codec = GetAudioCodec(mSendOff, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists<JsepAudioCodecDescription>(
      *codec, SdpRtcpFbAttributeList::kTransportCC);
  ASSERT_TRUE((codec = GetAudioCodec(mRecvAns, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists<JsepAudioCodecDescription>(
      *codec, SdpRtcpFbAttributeList::kTransportCC);
  ASSERT_TRUE((codec = GetAudioCodec(mSendAns, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists<JsepAudioCodecDescription>(
      *codec, SdpRtcpFbAttributeList::kTransportCC);
  ASSERT_TRUE((codec = GetAudioCodec(mRecvOff, 2, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists<JsepAudioCodecDescription>(
      *codec, SdpRtcpFbAttributeList::kTransportCC);
}

TEST_F(JsepTrackTest, AudioTransportCCFbSetUnsetWhenAnswerRejects) {
  InitCodecs({.enableAudioTransportCC = false});
  // Offer enables TransportCC, answer does not. After negotiation TC is
  // dropped; AudioCodecConfig::mTransportCCFbSet must reflect that even though
  // JsepAudioCodecDescription::mTransportCCEnabled stays true on the offerer.
  ((JsepAudioCodecDescription&)*mOffCodecs[0]).EnableTransportCC();
  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  for (const JsepTrack* track : {&mSendOff, &mRecvAns, &mSendAns, &mRecvOff}) {
    ASSERT_TRUE(track->GetNegotiatedDetails());
    std::vector<AudioCodecConfig> configs;
    dom::RTCRtpTransceiver::NegotiatedDetailsToAudioCodecConfigs(
        *track->GetNegotiatedDetails(), &configs);
    ASSERT_FALSE(configs.empty());
    for (const auto& config : configs) {
      EXPECT_FALSE(config.mTransportCCFbSet)
          << "codec " << config.mName
          << " has mTransportCCFbSet=true despite negotiation rejecting TC";
    }
  }
}

TEST_F(JsepTrackTest, AudioTransportCCFbSetWhenBothSidesNegotiate) {
  InitCodecs(CodecOverrides{});
  ((JsepAudioCodecDescription&)*mOffCodecs[0]).EnableTransportCC();
  ((JsepAudioCodecDescription&)*mAnsCodecs[0]).EnableTransportCC();
  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  for (const JsepTrack* track : {&mSendOff, &mRecvAns, &mSendAns, &mRecvOff}) {
    ASSERT_TRUE(track->GetNegotiatedDetails());
    std::vector<AudioCodecConfig> configs;
    dom::RTCRtpTransceiver::NegotiatedDetailsToAudioCodecConfigs(
        *track->GetNegotiatedDetails(), &configs);
    bool sawOpusWithTC = false;
    for (const auto& config : configs) {
      if (config.mName == "opus") {
        sawOpusWithTC = config.mTransportCCFbSet;
      }
    }
    EXPECT_TRUE(sawOpusWithTC)
        << "opus should have mTransportCCFbSet=true after both sides negotiate "
           "transport-cc";
  }
}

TEST_F(JsepTrackTest, VideoNegotiationOfferTransportCC) {
  InitCodecs({.offer = {.enableRemb = false, .enableTransportCC = true},
              .answer = {.enableRemb = false, .enableTransportCC = false}});
  // enable TransportCC on the offer codecs
  ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableTransportCC();
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // make sure TransportCC is on offer and not on answer
  ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 transport-cc"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 transport-cc"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepVideoCodecDescription> codec;
  ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
}

TEST_F(JsepTrackTest, VideoNegotiationAnswerTransportCC) {
  InitCodecs({.offer = {.enableRemb = false, .enableTransportCC = false},
              .answer = {.enableRemb = false, .enableTransportCC = true}});
  // enable TransportCC on the answer codecs
  ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableTransportCC();
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // make sure TransportCC is not on offer and not on answer
  ASSERT_EQ(mOffer->ToString().find("a=rtcp-fb:120 transport-cc"),
            std::string::npos);
  ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 transport-cc"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepVideoCodecDescription> codec;
  ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 0U);
}

TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerTransportCC) {
  InitCodecs({.enableRemb = false, .enableTransportCC = true});
  // enable TransportCC on the offer and answer codecs
  ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableTransportCC();
  ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableTransportCC();
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // make sure TransportCC is on offer and on answer
  ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 transport-cc"),
            std::string::npos);
  ASSERT_NE(mAnswer->ToString().find("a=rtcp-fb:120 transport-cc"),
            std::string::npos);
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  UniquePtr<JsepVideoCodecDescription> codec;
  ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC);
  ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC);
  ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ(codec->mOtherFbTypes.size(), 1U);
  CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC);
}

TEST_F(JsepTrackTest, AudioOffSendonlyAnsRecvonly) {
  Init(SdpMediaSection::kAudio);
  GetOffer().SetDirection(SdpDirectionAttribute::kSendonly);
  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(0);
}

TEST_F(JsepTrackTest, VideoOffSendonlyAnsRecvonly) {
  Init(SdpMediaSection::kVideo);
  GetOffer().SetDirection(SdpDirectionAttribute::kSendonly);
  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(0);
}

TEST_F(JsepTrackTest, AudioOffSendrecvAnsRecvonly) {
  Init(SdpMediaSection::kAudio);
  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(0);
}

TEST_F(JsepTrackTest, VideoOffSendrecvAnsRecvonly) {
  Init(SdpMediaSection::kVideo);
  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(0);
}

TEST_F(JsepTrackTest, AudioOffRecvonlyAnsSendonly) {
  Init(SdpMediaSection::kAudio);
  GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly);
  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
  OfferAnswer();
  CheckOffEncodingCount(0);
  CheckAnsEncodingCount(1);
}

TEST_F(JsepTrackTest, VideoOffRecvonlyAnsSendonly) {
  Init(SdpMediaSection::kVideo);
  GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly);
  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
  OfferAnswer();
  CheckOffEncodingCount(0);
  CheckAnsEncodingCount(1);
}

TEST_F(JsepTrackTest, AudioOffSendrecvAnsSendonly) {
  Init(SdpMediaSection::kAudio);
  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
  OfferAnswer();
  CheckOffEncodingCount(0);
  CheckAnsEncodingCount(1);
}

TEST_F(JsepTrackTest, VideoOffSendrecvAnsSendonly) {
  Init(SdpMediaSection::kVideo);
  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
  OfferAnswer();
  CheckOffEncodingCount(0);
  CheckAnsEncodingCount(1);
}

TEST_F(JsepTrackTest, DataChannelDraft05) {
  InitCodecs(CodecOverrides{});
  InitTracks(SdpMediaSection::kApplication);

  mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
  mOffer->AddMediaSection(SdpMediaSection::kApplication,
                          SdpDirectionAttribute::kSendrecv, 0,
                          SdpMediaSection::kDtlsSctp, sdp::kIPv4, "0.0.0.0");
  mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
  mAnswer->AddMediaSection(SdpMediaSection::kApplication,
                           SdpDirectionAttribute::kSendrecv, 0,
                           SdpMediaSection::kDtlsSctp, sdp::kIPv4, "0.0.0.0");

  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(std::string::npos,
            mOffer->ToString().find("a=sctpmap:5999 webrtc-datachannel 256"));
  ASSERT_NE(std::string::npos,
            mAnswer->ToString().find("a=sctpmap:5999 webrtc-datachannel 256"));
  // Note: this is testing for a workaround, see bug 1335262 for details
  ASSERT_NE(std::string::npos,
            mOffer->ToString().find("a=max-message-size:499"));
  ASSERT_NE(std::string::npos,
            mAnswer->ToString().find("a=max-message-size:499"));
  ASSERT_EQ(std::string::npos, mOffer->ToString().find("a=sctp-port"));
  ASSERT_EQ(std::string::npos, mAnswer->ToString().find("a=sctp-port"));
}

TEST_F(JsepTrackTest, DataChannelDraft21) {
  Init(SdpMediaSection::kApplication);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(std::string::npos, mOffer->ToString().find("a=sctp-port:5999"));
  ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=sctp-port:5999"));
  ASSERT_NE(std::string::npos,
            mOffer->ToString().find("a=max-message-size:499"));
  ASSERT_NE(std::string::npos,
            mAnswer->ToString().find("a=max-message-size:499"));
  ASSERT_EQ(std::string::npos, mOffer->ToString().find("a=sctpmap"));
  ASSERT_EQ(std::string::npos, mAnswer->ToString().find("a=sctpmap"));
}

TEST_F(JsepTrackTest, DataChannelDraft21AnswerWithDifferentPort) {
  InitCodecs(CodecOverrides{});

  mOffCodecs.pop_back();
  mOffCodecs.emplace_back(new JsepApplicationCodecDescription(
      "webrtc-datachannel", 256, 4555, 10544));

  InitTracks(SdpMediaSection::kApplication);
  InitSdp(SdpMediaSection::kApplication);

  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  ASSERT_NE(std::string::npos, mOffer->ToString().find("a=sctp-port:4555"));
  ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=sctp-port:5999"));
  ASSERT_NE(std::string::npos,
            mOffer->ToString().find("a=max-message-size:10544"));
  ASSERT_NE(std::string::npos,
            mAnswer->ToString().find("a=max-message-size:499"));
  ASSERT_EQ(std::string::npos, mOffer->ToString().find("a=sctpmap"));
  ASSERT_EQ(std::string::npos, mAnswer->ToString().find("a=sctpmap"));
}

TEST_F(JsepTrackTest, SimulcastRejected) {
  Init(SdpMediaSection::kVideo);
  std::vector<std::string> rids;
  rids.push_back("foo");
  rids.push_back("bar");
  mSendOff.SetRids(rids);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);
}

TEST_F(JsepTrackTest, SimulcastPrevented) {
  Init(SdpMediaSection::kVideo);
  std::vector<std::string> rids;
  rids.push_back("foo");
  rids.push_back("bar");
  mSendAns.SetRids(rids);
  OfferAnswer();
  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);
}

TEST_F(JsepTrackTest, SimulcastOfferer) {
  Init(SdpMediaSection::kVideo);
  std::vector<std::string> rids;
  rids.push_back("foo");
  rids.push_back("bar");
  mSendOff.SetRids(rids);
  CreateOffer();
  CreateAnswer();
  // Add simulcast/rid to answer
  mRecvAns.AddToMsection(rids, sdp::kRecv, mSsrcGenerator, false, &GetAnswer());
  Negotiate();
  ASSERT_TRUE(mSendOff.GetNegotiatedDetails());
  ASSERT_EQ(2U, mSendOff.GetNegotiatedDetails()->GetEncodingCount());
  ASSERT_EQ("foo", mSendOff.GetNegotiatedDetails()->GetEncoding(0).mRid);
  ASSERT_EQ("bar", mSendOff.GetNegotiatedDetails()->GetEncoding(1).mRid);
  ASSERT_NE(std::string::npos,
            mOffer->ToString().find("a=simulcast:send foo;bar"));
  ASSERT_NE(std::string::npos,
            mAnswer->ToString().find("a=simulcast:recv foo;bar"));
  ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo send"));
  ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar send"));
  ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo recv"));
  ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar recv"));
}

TEST_F(JsepTrackTest, SimulcastOffererWithRtx) {
  Init(SdpMediaSection::kVideo);
  std::vector<std::string> rids;
  rids.push_back("foo");
  rids.push_back("bar");
  rids.push_back("pop");
  mSendOff.SetRids(rids);
  mSendOff.AddToMsection(rids, sdp::kSend, mSsrcGenerator, true, &GetOffer());
  mRecvOff.AddToMsection(rids, sdp::kSend, mSsrcGenerator, true, &GetOffer());
  CreateAnswer();
  // Add simulcast/rid to answer
  mRecvAns.AddToMsection(rids, sdp::kRecv, mSsrcGenerator, false, &GetAnswer());
  Negotiate();

  ASSERT_EQ(3U, mSendOff.GetSsrcs().size());
  const auto posSsrc0 =
      mOffer->ToString().find(std::to_string(mSendOff.GetSsrcs()[0]));
  const auto posSsrc1 =
      mOffer->ToString().find(std::to_string(mSendOff.GetSsrcs()[1]));
  const auto posSsrc2 =
      mOffer->ToString().find(std::to_string(mSendOff.GetSsrcs()[2]));
  ASSERT_NE(std::string::npos, posSsrc0);
  ASSERT_NE(std::string::npos, posSsrc1);
  ASSERT_NE(std::string::npos, posSsrc2);
  ASSERT_GT(posSsrc1, posSsrc0);
  ASSERT_GT(posSsrc2, posSsrc0);
  ASSERT_GT(posSsrc2, posSsrc1);

  ASSERT_EQ(3U, mSendOff.GetRtxSsrcs().size());
  const auto posRtxSsrc0 =
      mOffer->ToString().find(std::to_string(mSendOff.GetRtxSsrcs()[0]));
  const auto posRtxSsrc1 =
      mOffer->ToString().find(std::to_string(mSendOff.GetRtxSsrcs()[1]));
  const auto posRtxSsrc2 =
      mOffer->ToString().find(std::to_string(mSendOff.GetRtxSsrcs()[2]));
  ASSERT_NE(std::string::npos, posRtxSsrc0);
  ASSERT_NE(std::string::npos, posRtxSsrc1);
  ASSERT_NE(std::string::npos, posRtxSsrc2);
  ASSERT_GT(posRtxSsrc1, posRtxSsrc0);
  ASSERT_GT(posRtxSsrc2, posRtxSsrc0);
  ASSERT_GT(posRtxSsrc2, posRtxSsrc1);
}

TEST_F(JsepTrackTest, SimulcastAnswerer) {
  Init(SdpMediaSection::kVideo);
  std::vector<std::string> rids;
  rids.push_back("foo");
  rids.push_back("bar");
  mSendAns.SetRids(rids);
  CreateOffer();
  // Add simulcast/rid to offer
  mRecvOff.AddToMsection(rids, sdp::kRecv, mSsrcGenerator, false, &GetOffer());
  CreateAnswer();
  Negotiate();
  ASSERT_TRUE(mSendAns.GetNegotiatedDetails());
  ASSERT_EQ(2U, mSendAns.GetNegotiatedDetails()->GetEncodingCount());
  ASSERT_EQ("foo", mSendAns.GetNegotiatedDetails()->GetEncoding(0).mRid);
  ASSERT_EQ("bar", mSendAns.GetNegotiatedDetails()->GetEncoding(1).mRid);
  ASSERT_NE(std::string::npos,
            mOffer->ToString().find("a=simulcast:recv foo;bar"));
  ASSERT_NE(std::string::npos,
            mAnswer->ToString().find("a=simulcast:send foo;bar"));
  ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo recv"));
  ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar recv"));
  ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo send"));
  ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar send"));
}

#define VERIFY_OPUS_MAX_PLAYBACK_RATE(track, expectedRate)          \
  {                                                                 \
    JsepTrack& copy(track);                                         \
    ASSERT_TRUE(copy.GetNegotiatedDetails());                       \
    ASSERT_TRUE(copy.GetNegotiatedDetails()->GetEncodingCount());   \
    for (const auto& codec :                                        \
         copy.GetNegotiatedDetails()->GetEncoding(0).GetCodecs()) { \
      if (codec->mName == "opus") {                                 \
        JsepAudioCodecDescription& audioCodec =                     \
            static_cast<JsepAudioCodecDescription&>(*codec);        \
        ASSERT_EQ((expectedRate), audioCodec.mMaxPlaybackRate);     \
      }                                                             \
    };                                                              \
  }

#define VERIFY_OPUS_FORCE_MONO(track, expected)                       \
  {                                                                   \
    JsepTrack& copy(track);                                           \
    ASSERT_TRUE(copy.GetNegotiatedDetails());                         \
    ASSERT_TRUE(copy.GetNegotiatedDetails()->GetEncodingCount());     \
    for (const auto& codec :                                          \
         copy.GetNegotiatedDetails()->GetEncoding(0).GetCodecs()) {   \
      if (codec->mName == "opus") {                                   \
        JsepAudioCodecDescription& audioCodec =                       \
            static_cast<JsepAudioCodecDescription&>(*codec);          \
        /* gtest has some compiler warnings when using ASSERT_EQ with \
         * booleans. */                                               \
        ASSERT_EQ((int)(expected), (int)audioCodec.mForceMono);       \
      }                                                               \
    };                                                                \
  }

TEST_F(JsepTrackTest, DefaultOpusParameters) {
  Init(SdpMediaSection::kAudio);
  OfferAnswer();

  VERIFY_OPUS_MAX_PLAYBACK_RATE(
      mSendOff, SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
  VERIFY_OPUS_MAX_PLAYBACK_RATE(
      mSendAns, SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U);
  VERIFY_OPUS_FORCE_MONO(mRecvOff, false);
  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 0U);
  VERIFY_OPUS_FORCE_MONO(mRecvAns, false);
}

TEST_F(JsepTrackTest, NonDefaultOpusParameters) {
  InitCodecs(CodecOverrides{});
  for (auto& codec : mAnsCodecs) {
    if (codec->mName == "opus") {
      JsepAudioCodecDescription* audioCodec =
          static_cast<JsepAudioCodecDescription*>(codec.get());
      audioCodec->mMaxPlaybackRate = 16000;
      audioCodec->mForceMono = true;
    }
  }
  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendOff, 16000U);
  VERIFY_OPUS_FORCE_MONO(mSendOff, true);
  VERIFY_OPUS_MAX_PLAYBACK_RATE(
      mSendAns, SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
  VERIFY_OPUS_FORCE_MONO(mSendAns, false);
  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U);
  VERIFY_OPUS_FORCE_MONO(mRecvOff, false);
  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 16000U);
  VERIFY_OPUS_FORCE_MONO(mRecvAns, true);
}

TEST_F(JsepTrackTest, RtcpFbWithPayloadTypeAsymmetry) {
  std::vector<std::string> expectedAckFbTypes;
  std::vector<std::string> expectedNackFbTypes{"", "pli"};
  std::vector<std::string> expectedCcmFbTypes{"fir"};
  std::vector<SdpRtcpFbAttributeList::Feedback> expectedOtherFbTypes{
      {"", SdpRtcpFbAttributeList::kRemb, "", ""},
      {"", SdpRtcpFbAttributeList::kTransportCC, "", ""}};

  InitCodecs(CodecOverrides{});

  // On offerer, configure to support remb and transport-cc on video codecs
  for (auto& codec : mOffCodecs) {
    if (codec->Type() == SdpMediaSection::kVideo) {
      auto& videoCodec = static_cast<JsepVideoCodecDescription&>(*codec);
      videoCodec.EnableRemb();
      videoCodec.EnableTransportCC();
    }
  }

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);

  CreateOffer();
  // We do not bother trying to bamboozle the answerer into doing asymmetric
  // payload types, we just use a raw SDP.
  const std::string answer =
      "v=0\r\n"
      "o=- 0 0 IN IP4 127.0.0.1\r\n"
      "s=-\r\n"
      "t=0 0\r\n"
      "a=msid-semantic:WMS *\r\n"
      "m=video 0 UDP/TLS/RTP/SAVPF 136\r\n"
      "c=IN IP4 0.0.0.0\r\n"
      "a=sendrecv\r\n"
      "a=fmtp:136 "
      "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode="
      "1\r\n"
      "a=msid:stream_id\r\n"
      "a=rtcp-fb:136 nack\r\n"
      "a=rtcp-fb:136 nack pli\r\n"
      "a=rtcp-fb:136 ccm fir\r\n"
      "a=rtcp-fb:136 goog-remb\r\n"
      "a=rtcp-fb:136 transport-cc\r\n"
      "a=rtpmap:136 H264/90000\r\n"
      "a=ssrc:2025549043 cname:\r\n";

  UniquePtr<SdpParser> parser(new SipccSdpParser);
  mAnswer = std::move(parser->Parse(answer)->Sdp());
  ASSERT_TRUE(mAnswer);

  mRecvOff.RecvTrackSetRemote(*mAnswer, GetAnswer());
  mRecvOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer());
  mSendOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer());

  ASSERT_TRUE(mSendOff.GetNegotiatedDetails());
  ASSERT_TRUE(mRecvOff.GetNegotiatedDetails());

  UniquePtr<JsepVideoCodecDescription> codec;
  ASSERT_TRUE((codec = GetVideoCodec(mSendOff)));
  ASSERT_EQ("136", codec->mDefaultPt)
      << "Offerer should have seen answer asymmetry!";
  ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 3, 0)));
  ASSERT_EQ("126", codec->mDefaultPt);
  ASSERT_EQ(expectedAckFbTypes, codec->mAckFbTypes);
  ASSERT_EQ(expectedNackFbTypes, codec->mNackFbTypes);
  ASSERT_EQ(expectedCcmFbTypes, codec->mCcmFbTypes);
  ASSERT_EQ(expectedOtherFbTypes, codec->mOtherFbTypes);
}

TEST_F(JsepTrackTest, OfferRedUlpfecNoRtx) {
  InitCodecs({.offer = {.addFecCodecs = true, .enableRtx = false},
              .answer = {.addFecCodecs = true, .enableRtx = true}});
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  for (const auto& type : {"VP8", "AV1", "H264"}) {
    for (const auto& [description, track] :
         {std::tuple{"Offerer send", &mSendOff},
          std::tuple{"Offerer recv", &mRecvOff},
          std::tuple{"Answerer send", &mSendAns},
          std::tuple{"Answerer recv", &mRecvAns}}) {
      auto codec = AsVideo(GetCodec(*track, type));
      ASSERT_TRUE(codec)
      << description << " track has codec for " << type;
      ASSERT_TRUE(codec->mFECEnabled)
      << description << " " << type << " has fec";
      ASSERT_FALSE(codec->mULPFECPayloadType.empty())
      << description << " " << type << " has ulpfec";
      ASSERT_FALSE(codec->mREDPayloadType.empty())
      << description << " " << type << " has red";
      ASSERT_TRUE(codec->mREDRTXPayloadType.empty())
      << description << " " << type << " does not have red/rtx";
    }
  }
}

TEST_F(JsepTrackTest, AnswerRedUlpfecNoRtx) {
  InitCodecs({.offer = {.addFecCodecs = true, .enableRtx = true},
              .answer = {.addFecCodecs = true, .enableRtx = false}});
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  CheckOffEncodingCount(1);
  CheckAnsEncodingCount(1);

  for (const auto& type : {"VP8", "AV1", "H264"}) {
    for (const auto& [description, track] :
         {std::tuple{"Offerer send", &mSendOff},
          std::tuple{"Offerer recv", &mRecvOff},
          std::tuple{"Answerer send", &mSendAns},
          std::tuple{"Answerer recv", &mRecvAns}}) {
      auto codec = AsVideo(GetCodec(*track, type));
      ASSERT_TRUE(codec)
      << description << " track has codec for " << type;
      ASSERT_TRUE(codec->mFECEnabled)
      << description << " " << type << " has fec";
      ASSERT_FALSE(codec->mULPFECPayloadType.empty())
      << description << " " << type << " has ulpfec";
      ASSERT_FALSE(codec->mREDPayloadType.empty())
      << description << " " << type << " has red";
      if (track != &mRecvOff) {
        ASSERT_TRUE(codec->mREDRTXPayloadType.empty())
        << description << " " << type << " does not have red/rtx";
      } else {
        ASSERT_FALSE(codec->mREDRTXPayloadType.empty())
        << description << " " << type
        << " has red/rtx, since it offered to receive it";
      }
    }
  }
}

TEST_F(JsepTrackTest, AudioSdpFmtpLine) {
  mOffCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});
  mAnsCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});
  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  // SanityCheck checks that the sdpFmtpLine for a local codec matches that of
  // the corresponding remote codec.
  UniquePtr<JsepAudioCodecDescription> codec;
  EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 0)));
  EXPECT_EQ("opus", codec->mName);
  EXPECT_EQ("maxplaybackrate=48000;stereo=1;useinbandfec=1",
            codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 0)));
  EXPECT_EQ("opus", codec->mName);
  EXPECT_EQ("maxplaybackrate=48000;stereo=1;useinbandfec=1",
            codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 1)));
  EXPECT_EQ("G722", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 1)));
  EXPECT_EQ("G722", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 2)));
  EXPECT_EQ("telephone-event", codec->mName);
  EXPECT_EQ("0-15", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 2)));
  EXPECT_EQ("telephone-event", codec->mName);
  EXPECT_EQ("0-15", codec->mSdpFmtpLine.valueOr("nothing"));
}

TEST_F(JsepTrackTest, NonDefaultAudioSdpFmtpLine) {
  mOffCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});
  mAnsCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});

  for (auto& codec : mOffCodecs) {
    if (codec->mName == "opus") {
      auto* audio = static_cast<JsepAudioCodecDescription*>(codec.get());
      audio->mForceMono = true;
      audio->mMaxPlaybackRate = 32000;
    }
  }

  for (auto& codec : mAnsCodecs) {
    if (codec->mName == "opus") {
      auto* audio = static_cast<JsepAudioCodecDescription*>(codec.get());
      audio->mFECEnabled = true;
      audio->mCbrEnabled = true;
      audio->mDTXEnabled = true;
      audio->mFrameSizeMs = 10;
      audio->mMinFrameSizeMs = 5;
      audio->mMaxFrameSizeMs = 20;
    }
  }

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);

  {
    // telephone-event doesn't store any params in JsepAudioCodecDescription.
    // Set them directly in the offer sdp instead.
    auto params = MakeUnique<SdpFmtpAttributeList::TelephoneEventParameters>();
    params->dtmfTones = "2-9";
    GetOffer().SetFmtp({"101", *params});
  }

  {
    // telephone-event doesn't store any params in JsepAudioCodecDescription.
    // Set them directly in the answer sdp instead.
    auto params = MakeUnique<SdpFmtpAttributeList::TelephoneEventParameters>();
    params->dtmfTones = "0-3,10";
    GetAnswer().SetFmtp({"101", *params});
  }

  OfferAnswer();

  // SanityCheck checks that the sdpFmtpLine for a local codec matches that of
  // the corresponding remote codec.
  UniquePtr<JsepAudioCodecDescription> codec;
  EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 0)));
  EXPECT_EQ("opus", codec->mName);
  EXPECT_EQ(
      "maxplaybackrate=48000;stereo=1;useinbandfec=1;usedtx=1;ptime=10;"
      "minptime=5;maxptime=20;cbr=1",
      codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 0)));
  EXPECT_EQ("opus", codec->mName);
  EXPECT_EQ("maxplaybackrate=32000;stereo=0;useinbandfec=1",
            codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 1)));
  EXPECT_EQ("G722", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 1)));
  EXPECT_EQ("G722", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 2)));
  EXPECT_EQ("telephone-event", codec->mName);
  EXPECT_EQ("0-3,10", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 2)));
  EXPECT_EQ("telephone-event", codec->mName);
  EXPECT_EQ("2-9", codec->mSdpFmtpLine.valueOr("nothing"));
}

TEST_F(JsepTrackTest, OpusPtimeNegotiatedFromRemoteFmtp) {
  Init(SdpMediaSection::kAudio);

  for (auto& codec : mOffCodecs) {
    if (codec->mName == "opus") {
      auto* audio = static_cast<JsepAudioCodecDescription*>(codec.get());
      audio->mFrameSizeMs = 40;
      audio->mMinFrameSizeMs = 10;
      audio->mMaxFrameSizeMs = 60;
      audio->mFECEnabled = true;
    }
  }

  InitTracks(SdpMediaSection::kAudio);
  InitSdp(SdpMediaSection::kAudio);
  OfferAnswer();

  UniquePtr<JsepAudioCodecDescription> codec;
  ASSERT_TRUE((codec = GetAudioCodec(mSendAns, 2, 0)));
  ASSERT_EQ("opus", codec->mName);
  ASSERT_EQ(40U, codec->mFrameSizeMs);
  ASSERT_EQ(10U, codec->mMinFrameSizeMs);
  ASSERT_EQ(60U, codec->mMaxFrameSizeMs);
}

TEST_F(JsepTrackTest, VideoSdpFmtpLine) {
  mOffCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});
  mAnsCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});
  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // SanityCheck checks that the sdpFmtpLine for a local codec matches that of
  // the corresponding remote codec.
  UniquePtr<JsepVideoCodecDescription> codec;
  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 0)));
  EXPECT_EQ("VP8", codec->mName);
  EXPECT_EQ("max-fs=12288;max-fr=60", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 0)));
  EXPECT_EQ("VP8", codec->mName);
  EXPECT_EQ("max-fs=12288;max-fr=60", codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 1)));
  EXPECT_EQ("H264", codec->mName);
  EXPECT_EQ(
      "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1",
      codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 1)));
  EXPECT_EQ("H264", codec->mName);
  EXPECT_EQ(
      "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1",
      codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 3)));
  EXPECT_EQ("red", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 3)));
  EXPECT_EQ("red", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 4)));
  EXPECT_EQ("ulpfec", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 4)));
  EXPECT_EQ("ulpfec", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
}

TEST_F(JsepTrackTest, NonDefaultVideoSdpFmtpLine) {
  mOffCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});
  mAnsCodecs = MakeCodecs(
      {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true});

  for (auto& codec : mOffCodecs) {
    if (codec->mName == "VP8" || codec->mName == "H264") {
      auto* video = static_cast<JsepVideoCodecDescription*>(codec.get());
      video->mConstraints.maxFs = 1200;
      if (codec->mName == "VP8") {
        video->mConstraints.maxFps = Some(15);
      } else {
        video->mConstraints.maxDpb = 6400;
        video->mConstraints.maxBr = 1000;
        JsepVideoCodecDescription::SetSaneH264Level(0x1F0,
                                                    &video->mProfileLevelId);
      }
    }
  }

  for (auto& codec : mAnsCodecs) {
    if (codec->mName == "VP8" || codec->mName == "H264") {
      auto* video = static_cast<JsepVideoCodecDescription*>(codec.get());
      video->mConstraints.maxFs = 32400;
      if (codec->mName == "VP8") {
        video->mConstraints.maxFps = Some(60);
      } else {
        video->mConstraints.maxMbps = 1944000;
        video->mConstraints.maxCpb = 800000;
        video->mConstraints.maxDpb = 128000;
        JsepVideoCodecDescription::SetSaneH264Level(0xAB,
                                                    &video->mProfileLevelId);
        video->mPacketizationMode = 1;
      }
    }
  }

  InitTracks(SdpMediaSection::kVideo);
  InitSdp(SdpMediaSection::kVideo);
  OfferAnswer();

  // SanityCheck checks that the sdpFmtpLine for a local codec matches that of
  // the corresponding remote codec.
  UniquePtr<JsepVideoCodecDescription> codec;
  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 0)));
  EXPECT_EQ("VP8", codec->mName);
  EXPECT_EQ("max-fs=32400;max-fr=60", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 0)));
  EXPECT_EQ("VP8", codec->mName);
  EXPECT_EQ("max-fs=1200;max-fr=15", codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 1)));
  EXPECT_EQ("H264", codec->mName);
  EXPECT_EQ(
      "profile-level-id=42f00b;level-asymmetry-allowed=1;packetization-mode=1;"
      "max-mbps=1944000;max-fs=32400;max-cpb=800000;max-dpb=128000",
      codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 1)));
  EXPECT_EQ("H264", codec->mName);
  EXPECT_EQ(
      "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1;"
      "max-fs=1200;max-dpb=6400;max-br=1000",
      codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 3)));
  EXPECT_EQ("red", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 3)));
  EXPECT_EQ("red", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));

  EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 5, 4)));
  EXPECT_EQ("ulpfec", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
  EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 5, 4)));
  EXPECT_EQ("ulpfec", codec->mName);
  EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing"));
}

TEST(JsepTrackRecvPayloadTypesTest, SingleTrackPTsAreUnique)
{
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  std::vector<UniquePtr<JsepCodecDescription>> codecs;
  codecs.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("1", "codec1", 48000, 1));

  SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& offer1Msection1 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  SipccSdp answer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& answer1Msection1 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  for (const auto& codec : codecs) {
    codec->mDirection = sdp::kSend;
    offer1Msection1.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection1);
  }

  JsepTrack t1{audio, sdp::Direction::kRecv};
  t1.PopulateCodecs(codecs, false);
  t1.RecvTrackSetLocal(offer1Msection1);
  t1.RecvTrackSetRemote(answer1, answer1Msection1);
  ASSERT_EQ(t1.Negotiate(answer1Msection1, answer1Msection1, offer1Msection1),
            NS_OK);

  std::vector tracks{&t1};
  JsepTrack::SetReceivePayloadTypes(tracks);
  EXPECT_THAT(t1.GetUniqueReceivePayloadTypes(), UnorderedElementsAre(1));
  EXPECT_THAT(t1.GetOtherReceivePayloadTypes(), UnorderedElementsAre());
}

TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsAreUnique)
{
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  std::vector<UniquePtr<JsepCodecDescription>> codecs1;
  codecs1.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("1", "codec1", 48000, 1));

  std::vector<UniquePtr<JsepCodecDescription>> codecs2;
  codecs2.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("2", "codec1", 48000, 1));

  SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& offer1Msection1 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& offer1Msection2 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  SipccSdp answer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& answer1Msection1 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& answer1Msection2 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  for (const auto& codec : codecs1) {
    codec->mDirection = sdp::kSend;
    offer1Msection1.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection1);
  }

  for (const auto& codec : codecs2) {
    codec->mDirection = sdp::kSend;
    offer1Msection2.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection2);
  }

  JsepTrack t1{audio, sdp::Direction::kRecv};
  t1.PopulateCodecs(codecs1, false);
  t1.RecvTrackSetLocal(offer1Msection1);
  t1.RecvTrackSetRemote(answer1, answer1Msection1);
  ASSERT_EQ(t1.Negotiate(answer1Msection1, answer1Msection1, offer1Msection1),
            NS_OK);

  JsepTrack t2{audio, sdp::Direction::kRecv};
  t2.PopulateCodecs(codecs2, false);
  t2.RecvTrackSetLocal(offer1Msection2);
  t2.RecvTrackSetRemote(answer1, answer1Msection2);
  ASSERT_EQ(t2.Negotiate(answer1Msection2, answer1Msection2, offer1Msection2),
            NS_OK);

  std::vector tracks{&t1, &t2};
  JsepTrack::SetReceivePayloadTypes(tracks);
  EXPECT_THAT(t1.GetUniqueReceivePayloadTypes(), UnorderedElementsAre(1));
  EXPECT_THAT(t1.GetOtherReceivePayloadTypes(), UnorderedElementsAre(2));
  EXPECT_THAT(t2.GetUniqueReceivePayloadTypes(), UnorderedElementsAre(2));
  EXPECT_THAT(t2.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1));
}

TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsAreDuplicates)
{
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  std::vector<UniquePtr<JsepCodecDescription>> codecs1;
  codecs1.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("1", "codec1", 48000, 1));

  std::vector<UniquePtr<JsepCodecDescription>> codecs2;
  codecs2.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("1", "codec1", 48000, 1));

  SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& offer1Msection1 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& offer1Msection2 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  SipccSdp answer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& answer1Msection1 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& answer1Msection2 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  for (const auto& codec : codecs1) {
    codec->mDirection = sdp::kSend;
    offer1Msection1.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection1);
  }
  for (const auto& codec : codecs2) {
    codec->mDirection = sdp::kSend;
    offer1Msection2.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection2);
  }

  JsepTrack t1{audio, sdp::Direction::kRecv};
  t1.PopulateCodecs(codecs1, false);
  t1.RecvTrackSetLocal(offer1Msection1);
  t1.RecvTrackSetRemote(answer1, answer1Msection1);
  ASSERT_EQ(t1.Negotiate(answer1Msection1, answer1Msection1, offer1Msection1),
            NS_OK);

  JsepTrack t2{audio, sdp::Direction::kRecv};
  t2.PopulateCodecs(codecs2, false);
  t2.RecvTrackSetLocal(offer1Msection2);
  t2.RecvTrackSetRemote(answer1, answer1Msection2);
  ASSERT_EQ(t2.Negotiate(answer1Msection2, answer1Msection2, offer1Msection2),
            NS_OK);

  std::vector tracks{&t1, &t2};
  JsepTrack::SetReceivePayloadTypes(tracks);
  EXPECT_THAT(t1.GetUniqueReceivePayloadTypes(), UnorderedElementsAre());
  EXPECT_THAT(t1.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1));
  EXPECT_THAT(t2.GetUniqueReceivePayloadTypes(), UnorderedElementsAre());
  EXPECT_THAT(t2.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1));
}

TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsOverlap)
{
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  std::vector<UniquePtr<JsepCodecDescription>> codecs1;
  codecs1.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("1", "codec1", 48000, 1));
  codecs1.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("2", "codec2", 48000, 1));

  std::vector<UniquePtr<JsepCodecDescription>> codecs2;
  codecs2.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("1", "codec1", 48000, 1));
  codecs2.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("3", "codec2", 48000, 1));

  SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& offer1Msection1 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& offer1Msection2 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  SipccSdp answer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& answer1Msection1 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& answer1Msection2 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  for (const auto& codec : codecs1) {
    codec->mDirection = sdp::kSend;
    offer1Msection1.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection1);
  }

  for (const auto& codec : codecs2) {
    codec->mDirection = sdp::kSend;
    offer1Msection2.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection2);
  }

  JsepTrack t1{audio, sdp::Direction::kRecv};
  t1.PopulateCodecs(codecs1, false);
  t1.RecvTrackSetLocal(offer1Msection1);
  t1.RecvTrackSetRemote(answer1, answer1Msection1);
  ASSERT_EQ(t1.Negotiate(answer1Msection1, answer1Msection1, offer1Msection1),
            NS_OK);

  JsepTrack t2{audio, sdp::Direction::kRecv};
  t2.PopulateCodecs(codecs2, false);
  t2.RecvTrackSetLocal(offer1Msection2);
  t2.RecvTrackSetRemote(answer1, answer1Msection2);
  ASSERT_EQ(t2.Negotiate(answer1Msection2, answer1Msection2, offer1Msection2),
            NS_OK);

  std::vector tracks{&t1, &t2};
  JsepTrack::SetReceivePayloadTypes(tracks);
  EXPECT_THAT(t1.GetUniqueReceivePayloadTypes(), UnorderedElementsAre(2));
  EXPECT_THAT(t1.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1, 3));
  EXPECT_THAT(t2.GetUniqueReceivePayloadTypes(), UnorderedElementsAre(3));
  EXPECT_THAT(t2.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1, 2));
}

TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsDuplicateAfterRenegotiation)
{
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  std::vector<UniquePtr<JsepCodecDescription>> codecs1;
  codecs1.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("1", "codec1", 48000, 1));
  codecs1.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("2", "codec2", 48000, 1));

  std::vector<UniquePtr<JsepCodecDescription>> codecs2;
  codecs2.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("3", "codec1", 48000, 1));
  codecs2.emplace_back(
      MakeUnique<JsepAudioCodecDescription>("4", "codec2", 48000, 1));

  // First negotiation.
  SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& offer1Msection1 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& offer1Msection2 = offer1.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  SipccSdp answer1(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& answer1Msection1 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& answer1Msection2 = answer1.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  for (const auto& codec : codecs1) {
    codec->mDirection = sdp::kSend;
    offer1Msection1.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection1);
  }

  for (const auto& codec : codecs2) {
    codec->mDirection = sdp::kSend;
    offer1Msection2.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer1Msection2);
  }

  // t1 and t2 use distinct payload types in the first negotiation.
  JsepTrack t1{audio, sdp::Direction::kRecv};
  t1.PopulateCodecs(codecs1, false);
  t1.RecvTrackSetLocal(offer1Msection1);
  t1.RecvTrackSetRemote(answer1, answer1Msection1);
  ASSERT_EQ(t1.Negotiate(answer1Msection1, answer1Msection1, offer1Msection1),
            NS_OK);

  JsepTrack t2{audio, sdp::Direction::kRecv};
  t2.PopulateCodecs(codecs2, false);
  t2.RecvTrackSetLocal(offer1Msection2);
  t2.RecvTrackSetRemote(answer1, answer1Msection2);
  ASSERT_EQ(t2.Negotiate(answer1Msection2, answer1Msection2, offer1Msection2),
            NS_OK);

  std::vector tracks{&t1, &t2};
  JsepTrack::SetReceivePayloadTypes(tracks);
  EXPECT_THAT(t1.GetUniqueReceivePayloadTypes(), UnorderedElementsAre(1, 2));
  EXPECT_THAT(t1.GetOtherReceivePayloadTypes(), UnorderedElementsAre(3, 4));
  EXPECT_THAT(t2.GetUniqueReceivePayloadTypes(), UnorderedElementsAre(3, 4));
  EXPECT_THAT(t2.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1, 2));

  // Second negotiation.
  SipccSdp offer2(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& offer2Msection1 = offer2.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& offer2Msection2 = offer2.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  SipccSdp answer2(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& answer2Msection1 = answer2.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  SdpMediaSection& answer2Msection2 = answer2.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  for (const auto& codec : codecs1) {
    codec->mDirection = sdp::kSend;
    offer2Msection1.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer2Msection1);
  }

  for (const auto& codec : codecs2) {
    codec->mDirection = sdp::kSend;
    offer2Msection2.AddCodec(codec->mDefaultPt, codec->mName, codec->mClock,
                             codec->mChannels);
    UniquePtr<JsepCodecDescription> clone = codec->Clone();
    clone->mDirection = sdp::kRecv;
    clone->AddToMediaSection(answer2Msection2);
  }

  t1.PopulateCodecs(codecs1, false);
  t1.RecvTrackSetLocal(offer2Msection1);
  t1.RecvTrackSetRemote(answer2, answer2Msection1);
  ASSERT_EQ(t1.Negotiate(answer2Msection1, answer2Msection1, offer2Msection1),
            NS_OK);

  // Change t2 to use the same payload types as t1. Both tracks should now mark
  // all their payload types as duplicates.
  t2.PopulateCodecs(codecs1, false);
  t2.RecvTrackSetLocal(offer2Msection2);
  t2.RecvTrackSetRemote(answer2, answer2Msection2);
  ASSERT_EQ(t2.Negotiate(answer2Msection2, answer2Msection2, offer2Msection2),
            NS_OK);

  std::vector newTracks{&t1, &t2};
  JsepTrack::SetReceivePayloadTypes(newTracks);
  EXPECT_THAT(t1.GetUniqueReceivePayloadTypes(), UnorderedElementsAre());
  EXPECT_THAT(t1.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1, 2));
  EXPECT_THAT(t2.GetUniqueReceivePayloadTypes(), UnorderedElementsAre());
  EXPECT_THAT(t2.GetOtherReceivePayloadTypes(), UnorderedElementsAre(1, 2));
}

TEST(JsepTrackRecvSsrcTest, SsrcExtractionOnSendingMsection)
{
  // A sending remote m-section with a=ssrc lines should have those SSRCs
  // captured as expected receive SSRCs.
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  SipccSdp sdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& msection = sdp.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  msection.SetSsrcs({12345678}, "test-cname");

  JsepTrack t{audio, sdp::kRecv};
  t.RecvTrackSetRemote(sdp, msection);

  ASSERT_EQ(t.GetSsrcs().size(), 1u);
  EXPECT_EQ(t.GetSsrcs().front(), 12345678u);
}

TEST(JsepTrackRecvSsrcTest, SsrcExtractionOnNonSendingMsection)
{
  // A non-sending (recvonly) remote m-section with a=ssrc lines is unusual
  // but the SSRCs are extracted unless they duplicate our own send SSRCs.
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  SipccSdp sdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& msection = sdp.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  msection.SetSsrcs({12345678}, "test-cname");

  JsepTrack t{audio, sdp::kRecv};
  // No own send SSRCs → no collision → SSRC is retained.
  t.RecvTrackSetRemote(sdp, msection);

  ASSERT_EQ(t.GetSsrcs().size(), 1u);
  EXPECT_EQ(t.GetSsrcs().front(), 12345678u);
}

TEST(JsepTrackRecvSsrcTest, DuplicateSendSsrcFiltered)
{
  // If a remote a=ssrc value duplicates one of our own send SSRCs, it must
  // be filtered out to prevent EnsureLocalSSRC() from regenerating our send
  // SSRC to a value the peer never negotiated.
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  SipccSdp sdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& msection = sdp.AddMediaSection(
      audio, SdpDirectionAttribute::kRecvonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  // Remote places two SSRCs in its recvonly section (unusual but seen with
  // FaceTime): one collides with our send SSRC, one does not.
  msection.SetSsrcs({12345678, 99999999}, "test-cname");

  JsepTrack t{audio, sdp::kRecv};
  // 12345678 is one of our own send SSRCs — it should be filtered out.
  t.RecvTrackSetRemote(sdp, msection, {12345678});

  ASSERT_EQ(t.GetSsrcs().size(), 1u);
  EXPECT_EQ(t.GetSsrcs().front(), 99999999u);
}

TEST(JsepTrackRecvSsrcTest, MultipleUniqueSsrcsExtracted)
{
  // All unique SSRCs from a sending m-section should be captured in order.
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  SipccSdp sdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& msection = sdp.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");
  msection.SetSsrcs({11111111, 22222222, 33333333}, "test-cname");

  JsepTrack t{audio, sdp::kRecv};
  t.RecvTrackSetRemote(sdp, msection);

  ASSERT_EQ(t.GetSsrcs().size(), 3u);
  EXPECT_EQ(t.GetSsrcs()[0], 11111111u);
  EXPECT_EQ(t.GetSsrcs()[1], 22222222u);
  EXPECT_EQ(t.GetSsrcs()[2], 33333333u);
}

TEST(JsepTrackRecvSsrcTest, DuplicateSsrcLinesDeduped)
{
  // The same SSRC can appear on multiple a=ssrc lines with different
  // attributes (e.g., cname on one line, msid on another). It should only
  // be added to mSsrcs once.
  constexpr auto audio = SdpMediaSection::MediaType::kAudio;

  SipccSdp sdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""));
  SdpMediaSection& msection = sdp.AddMediaSection(
      audio, SdpDirectionAttribute::kSendonly, 0,
      SdpHelper::GetProtocolForMediaType(audio), sdp::kIPv4, "0.0.0.0");

  auto ssrcAttr = MakeUnique<SdpSsrcAttributeList>();
  ssrcAttr->PushEntry(12345678, "cname:test-cname");
  ssrcAttr->PushEntry(12345678, "msid:stream1 track1");
  ssrcAttr->PushEntry(87654321, "cname:test-cname");
  ssrcAttr->PushEntry(87654321, "msid:stream1 track2");
  msection.GetAttributeList().SetAttribute(std::move(ssrcAttr));

  JsepTrack t{audio, sdp::kRecv};
  t.RecvTrackSetRemote(sdp, msection);

  ASSERT_EQ(t.GetSsrcs().size(), 2u);
  EXPECT_EQ(t.GetSsrcs()[0], 12345678u);
  EXPECT_EQ(t.GetSsrcs()[1], 87654321u);
}

}  // namespace mozilla
