/*
 *  Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "media/base/stream_params.h"
#include "modules/rtp_rtcp/source/byte_io.h"
#include "modules/rtp_rtcp/source/rtp_util.h"
#include "pc/media_session.h"
#include "pc/session_description.h"
#include "test/field_trial.h"
#include "test/gmock.h"
#include "test/gtest.h"
#include "test/peer_scenario/peer_scenario.h"

namespace webrtc {
namespace test {
namespace {

enum class MidTestConfiguration {
  // Legacy endpoint setup where PT demuxing is used.
  kMidNotNegotiated,
  // MID is negotiated but missing from packets. PT demuxing is disabled, so
  // SSRCs have to be added to the SDP for WebRTC to forward packets correctly.
  // Happens when client is spec compliant but the SFU isn't. Popular legacy.
  kMidNegotiatedButMissingFromPackets,
  // Fully spec-compliant: MID is present so we can safely drop packets with
  // unknown MIDs.
  kMidNegotiatedAndPresentInPackets,
};

// Gives the parameterized test a readable suffix.
std::string TestParametersMidTestConfigurationToString(
    testing::TestParamInfo<MidTestConfiguration> info) {
  switch (info.param) {
    case MidTestConfiguration::kMidNotNegotiated:
      return "MidNotNegotiated";
    case MidTestConfiguration::kMidNegotiatedButMissingFromPackets:
      return "MidNegotiatedButMissingFromPackets";
    case MidTestConfiguration::kMidNegotiatedAndPresentInPackets:
      return "MidNegotiatedAndPresentInPackets";
  }
}

class FrameObserver : public rtc::VideoSinkInterface<VideoFrame> {
 public:
  FrameObserver() : frame_observed_(false) {}
  void OnFrame(const VideoFrame&) override { frame_observed_ = true; }

  std::atomic<bool> frame_observed_;
};

uint32_t get_ssrc(SessionDescriptionInterface* offer, size_t track_index) {
  EXPECT_LT(track_index, offer->description()->contents().size());
  return offer->description()
      ->contents()[track_index]
      .media_description()
      ->streams()[0]
      .ssrcs[0];
}

void set_ssrc(SessionDescriptionInterface* offer, size_t index, uint32_t ssrc) {
  EXPECT_LT(index, offer->description()->contents().size());
  cricket::StreamParams& new_stream_params = offer->description()
                                                 ->contents()[index]
                                                 .media_description()
                                                 ->mutable_streams()[0];
  new_stream_params.ssrcs[0] = ssrc;
  new_stream_params.ssrc_groups[0].ssrcs[0] = ssrc;
}

}  // namespace

class UnsignaledStreamTest
    : public ::testing::Test,
      public ::testing::WithParamInterface<MidTestConfiguration> {};

TEST_P(UnsignaledStreamTest, ReplacesUnsignaledStreamOnCompletedSignaling) {
  // This test covers a scenario that might occur if a remote client starts
  // sending media packets before negotiation has completed. Depending on setup,
  // these packets either get dropped or trigger an unsignalled default stream
  // to be created, and connects that to a default video sink.
  // In some edge cases using Unified Plan and PT demuxing, the default stream
  // is create in a different transceiver to where the media SSRC will actually
  // be used. This test verifies that the default stream is removed properly,
  // and that packets are demuxed and video frames reach the desired sink.
  const MidTestConfiguration kMidTestConfiguration = GetParam();

  // Defined before PeerScenario so it gets destructed after, to avoid use after
  // free.
  PeerScenario s(*::testing::UnitTest::GetInstance()->current_test_info());

  PeerScenarioClient::Config config = PeerScenarioClient::Config();
  // Disable encryption so that we can inject a fake early media packet without
  // triggering srtp failures.
  auto* caller = s.CreateClient(config);
  auto* callee = s.CreateClient(config);

  auto send_node = s.net()->NodeBuilder().Build().node;
  auto ret_node = s.net()->NodeBuilder().Build().node;

  s.net()->CreateRoute(caller->endpoint(), {send_node}, callee->endpoint());
  s.net()->CreateRoute(callee->endpoint(), {ret_node}, caller->endpoint());

  auto signaling = s.ConnectSignaling(caller, callee, {send_node}, {ret_node});
  PeerScenarioClient::VideoSendTrackConfig video_conf;
  video_conf.generator.squares_video->framerate = 15;

  auto first_track = caller->CreateVideo("VIDEO", video_conf);
  FrameObserver first_sink;
  callee->AddVideoReceiveSink(first_track.track->id(), &first_sink);

  signaling.StartIceSignaling();
  std::atomic<bool> offer_exchange_done(false);
  std::atomic<bool> got_unsignaled_packet(false);

  // We will capture the media ssrc of the first added stream, and preemptively
  // inject a new media packet using a different ssrc. What happens depends on
  // the test configuration.
  //
  // MidTestConfiguration::kMidNotNegotiated:
  // - MID is not negotiated which means PT-based demuxing is enabled. Because
  //   the packets have no MID, the second ssrc packet gets forwarded to the
  //   first m= section. This will create a "default stream" for the second ssrc
  //   and connect it to the default video sink (not set in this test). The test
  //   verifies we can recover from this when we later get packets for the first
  //   ssrc.
  //
  // MidTestConfiguration::kMidNegotiatedButMissingFromPackets:
  // - MID is negotiated wich means PT-based demuxing is disabled. Because we
  //   modify the packets not to contain the MID anyway (simulating a legacy SFU
  //   that does not negotiate properly) unknown SSRCs are dropped but do not
  //   otherwise cause any issues.
  //
  // MidTestConfiguration::kMidNegotiatedAndPresentInPackets:
  // - MID is negotiated which means PT-based demuxing is enabled. In this case
  //   the packets have the MID so they either get forwarded or dropped
  //   depending on if the MID is known. The spec-compliant way is also the most
  //   straight-forward one.

  uint32_t first_ssrc = 0;
  uint32_t second_ssrc = 0;
  absl::optional<int> mid_header_extension_id = absl::nullopt;

  signaling.NegotiateSdp(
      /* munge_sdp = */
      [&](SessionDescriptionInterface* offer) {
        // Obtain the MID header extension ID and if we want the
        // MidTestConfiguration::kMidNotNegotiated setup then we remove the MID
        // header extension through SDP munging (otherwise SDP is not modified).
        for (cricket::ContentInfo& content_info :
             offer->description()->contents()) {
          std::vector<RtpExtension> header_extensions =
              content_info.media_description()->rtp_header_extensions();
          for (auto it = header_extensions.begin();
               it != header_extensions.end(); ++it) {
            if (it->uri == RtpExtension::kMidUri) {
              // MID header extension found!
              mid_header_extension_id = it->id;
              if (kMidTestConfiguration ==
                  MidTestConfiguration::kMidNotNegotiated) {
                // Munge away the extension.
                header_extensions.erase(it);
              }
              break;
            }
          }
          content_info.media_description()->set_rtp_header_extensions(
              std::move(header_extensions));
        }
        ASSERT_TRUE(mid_header_extension_id.has_value());
      },
      /* modify_sdp = */
      [&](SessionDescriptionInterface* offer) {
        first_ssrc = get_ssrc(offer, 0);
        second_ssrc = first_ssrc + 1;

        send_node->router()->SetWatcher([&](const EmulatedIpPacket& packet) {
          if (IsRtpPacket(packet.data) &&
              ByteReader<uint32_t>::ReadBigEndian(&(packet.cdata()[8])) ==
                  first_ssrc &&
              !got_unsignaled_packet) {
            // Parse packet and modify the SSRC to simulate a second m=
            // section that has not been negotiated yet.
            std::vector<RtpExtension> extensions;
            extensions.emplace_back(RtpExtension::kMidUri,
                                    mid_header_extension_id.value());
            RtpHeaderExtensionMap extensions_map(extensions);
            RtpPacket parsed_packet;
            parsed_packet.IdentifyExtensions(extensions_map);
            ASSERT_TRUE(parsed_packet.Parse(packet.data));
            parsed_packet.SetSsrc(second_ssrc);
            // The MID extension is present if and only if it was negotiated.
            // If present, we either want to remove it or modify it depending
            // on setup.
            switch (kMidTestConfiguration) {
              case MidTestConfiguration::kMidNotNegotiated:
                EXPECT_FALSE(parsed_packet.HasExtension<RtpMid>());
                break;
              case MidTestConfiguration::kMidNegotiatedButMissingFromPackets:
                EXPECT_TRUE(parsed_packet.HasExtension<RtpMid>());
                ASSERT_TRUE(parsed_packet.RemoveExtension(RtpMid::kId));
                break;
              case MidTestConfiguration::kMidNegotiatedAndPresentInPackets:
                EXPECT_TRUE(parsed_packet.HasExtension<RtpMid>());
                // The simulated second m= section would have a different MID.
                // If we don't modify it here then `second_ssrc` would end up
                // being mapped to the first m= section which would cause SSRC
                // conflicts if we later add the same SSRC to a second m=
                // section. Hidden assumption: first m= section does not use
                // MID:1.
                ASSERT_TRUE(parsed_packet.SetExtension<RtpMid>("1"));
                break;
            }
            // Inject the modified packet.
            rtc::CopyOnWriteBuffer updated_buffer = parsed_packet.Buffer();
            EmulatedIpPacket updated_packet(
                packet.from, packet.to, updated_buffer, packet.arrival_time);
            send_node->OnPacketReceived(std::move(updated_packet));
            got_unsignaled_packet = true;
          }
        });
      },
      [&](const SessionDescriptionInterface& answer) {
        EXPECT_EQ(answer.description()->contents().size(), 1u);
        offer_exchange_done = true;
      });
  EXPECT_TRUE(s.WaitAndProcess(&offer_exchange_done));
  EXPECT_TRUE(s.WaitAndProcess(&got_unsignaled_packet));
  EXPECT_TRUE(s.WaitAndProcess(&first_sink.frame_observed_));

  auto second_track = caller->CreateVideo("VIDEO2", video_conf);
  FrameObserver second_sink;
  callee->AddVideoReceiveSink(second_track.track->id(), &second_sink);

  // Create a second video stream, munge the sdp to force it to use our fake
  // early media ssrc.
  offer_exchange_done = false;
  signaling.NegotiateSdp(
      /* munge_sdp = */
      [&](SessionDescriptionInterface* offer) {
        set_ssrc(offer, 1, second_ssrc);
      },
      /* modify_sdp = */ {},
      [&](const SessionDescriptionInterface& answer) {
        EXPECT_EQ(answer.description()->contents().size(), 2u);
        offer_exchange_done = true;
      });
  EXPECT_TRUE(s.WaitAndProcess(&offer_exchange_done));
  EXPECT_TRUE(s.WaitAndProcess(&second_sink.frame_observed_));
  caller->pc()->Close();
  callee->pc()->Close();
}

INSTANTIATE_TEST_SUITE_P(
    All,
    UnsignaledStreamTest,
    ::testing::Values(MidTestConfiguration::kMidNotNegotiated,
                      MidTestConfiguration::kMidNegotiatedButMissingFromPackets,
                      MidTestConfiguration::kMidNegotiatedAndPresentInPackets),
    TestParametersMidTestConfigurationToString);

}  // namespace test
}  // namespace webrtc
