// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/mirroring/service/openscreen_session_host.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/mirroring/service/fake_socket_factory.h"
#include "components/mirroring/service/fake_video_capture_host.h"
#include "components/mirroring/service/mirror_settings.h"
#include "components/mirroring/service/mirroring_features.h"
#include "media/base/audio_codecs.h"
#include "media/base/media_switches.h"
#include "media/base/video_codecs.h"
#include "media/cast/cast_config.h"
#include "media/cast/common/openscreen_conversion_helpers.h"
#include "media/cast/encoding/encoding_support.h"
#include "media/cast/sender/audio_sender.h"
#include "media/cast/test/openscreen_test_helpers.h"
#include "media/cast/test/utility/default_config.h"
#include "media/media_buildflags.h"
#include "media/video/video_decode_accelerator.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/ip_address.h"
#include "services/network/public/mojom/clear_data_filter.mojom.h"
#include "services/viz/public/cpp/gpu/gpu.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/jsoncpp/source/include/json/reader.h"
#include "third_party/jsoncpp/source/include/json/writer.h"
#include "third_party/openscreen/src/cast/streaming/message_fields.h"
#include "third_party/openscreen/src/cast/streaming/public/offer_messages.h"
#include "third_party/openscreen/src/cast/streaming/remoting_capabilities.h"
#include "third_party/openscreen/src/cast/streaming/sender_message.h"
#include "third_party/openscreen/src/cast/streaming/ssrc.h"

using media::cast::FrameSenderConfig;
using media::cast::MockSender;
using media::mojom::RemotingSinkMetadata;
using media::mojom::RemotingSinkMetadataPtr;
using media::mojom::RemotingStartFailReason;
using media::mojom::RemotingStopReason;
using mirroring::mojom::SessionError;
using mirroring::mojom::SessionType;
using openscreen::ErrorOr;
using openscreen::cast::Offer;
using openscreen::cast::SenderMessage;
using openscreen::cast::VideoStream;
using ::testing::_;
using ::testing::AtLeast;
using ::testing::InvokeWithoutArgs;
using ::testing::Mock;
using ::testing::NiceMock;

namespace mirroring {

namespace {

const openscreen::cast::Answer kAnswerWithConstraints{
    1234,
    // Send indexes and SSRCs are set later.
    {},
    {},
    openscreen::cast::Constraints{
        openscreen::cast::AudioConstraints{44100, 2, 32000, 960000,
                                           std::chrono::milliseconds(4000)},
        openscreen::cast::VideoConstraints{
            40000.0, openscreen::cast::Dimensions{320, 480, {30, 1}},
            openscreen::cast::Dimensions{1920, 1080, {60, 1}}, 300000,
            144000000, std::chrono::milliseconds(4000)}},
    openscreen::cast::DisplayDescription{
        openscreen::cast::Dimensions{1280, 720, {60, 1}},
        openscreen::cast::AspectRatio{16, 9},
        openscreen::cast::AspectRatioConstraint::kFixed,
    },
};

class MockRemotingSource : public media::mojom::RemotingSource {
 public:
  MockRemotingSource() = default;
  ~MockRemotingSource() override = default;

  void Bind(mojo::PendingReceiver<media::mojom::RemotingSource> receiver) {
    receiver_.Bind(std::move(receiver));
  }

  void reset_on_disconnect() {
    receiver_.set_disconnect_handler(
        base::BindOnce(&MockRemotingSource::reset, weak_factory_.GetWeakPtr()));
  }

  void reset() { receiver_.reset(); }

  MOCK_METHOD0(OnSinkGone, void());
  MOCK_METHOD0(OnStarted, void());
  MOCK_METHOD1(OnStartFailed, void(RemotingStartFailReason));
  MOCK_METHOD1(OnMessageFromSink, void(const std::vector<uint8_t>&));
  MOCK_METHOD1(OnStopped, void(RemotingStopReason));
  MOCK_METHOD1(OnSinkAvailable, void(const RemotingSinkMetadata&));
  void OnSinkAvailable(RemotingSinkMetadataPtr metadata) override {
    OnSinkAvailable(*metadata);
  }

 private:
  mojo::Receiver<media::mojom::RemotingSource> receiver_{this};
  base::WeakPtrFactory<MockRemotingSource> weak_factory_{this};
};

Json::Value ParseAsJsoncppValue(std::string_view document) {
  Json::CharReaderBuilder builder;
  Json::CharReaderBuilder::strictMode(&builder.settings_);
  EXPECT_FALSE(document.empty());

  Json::Value root_node;
  std::string error_msg;
  std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
  EXPECT_TRUE(reader->parse(&*document.begin(), &*document.end(), &root_node,
                            &error_msg));

  return root_node;
}

std::string Stringify(const Json::Value& value) {
  EXPECT_FALSE(value.empty());
  Json::StreamWriterBuilder factory;
  factory["indentation"] = "";

  std::unique_ptr<Json::StreamWriter> const writer(factory.newStreamWriter());
  std::ostringstream stream;
  writer->write(value, &stream);

  EXPECT_TRUE(stream);
  return stream.str();
}

openscreen::cast::SenderStats ConstructDefaultSenderStats() {
  return openscreen::cast::SenderStats{
      .audio_statistics = openscreen::cast::SenderStats::StatisticsList(),
      .audio_histograms = openscreen::cast::SenderStats::HistogramsList(),
      .video_statistics = openscreen::cast::SenderStats::StatisticsList(),
      .video_histograms = openscreen::cast::SenderStats::HistogramsList()};
}

}  // namespace

class OpenscreenSessionHostTest : public mojom::ResourceProvider,
                                  public mojom::SessionObserver,
                                  public mojom::CastMessageChannel,
                                  public ::testing::Test {
 public:
  OpenscreenSessionHostTest() = default;

  OpenscreenSessionHostTest(const OpenscreenSessionHostTest&) = delete;
  OpenscreenSessionHostTest& operator=(const OpenscreenSessionHostTest&) =
      delete;

  void TearDown() override {
    media::cast::encoding_support::ClearHardwareCodecDenyListForTesting();
  }

  void OnSessionHostDeletion() {
    ASSERT_TRUE(session_host_deletion_cb_);
    if (session_host_deletion_cb_) {
      std::move(session_host_deletion_cb_).Run();
    }
  }

  ~OpenscreenSessionHostTest() override {
    // We may have already deleted the session host if the session was stopped.
    if (session_host_) {
      DeleteSessionHost();
    }
  }

 protected:
  void PauseCapturingVideo() { session_host().PauseCapturingVideo(); }

  bool TryResumeCapturingVideo() {
    return session_host().TryResumeCapturingVideo();
  }

  MirrorSettings* mirror_settings() { return &session_host_->mirror_settings_; }

  // mojom::SessionObserver implementation.
  MOCK_METHOD(void, OnError, (SessionError));
  MOCK_METHOD(void, DidStart, ());
  MOCK_METHOD(void, DidStop, ());
  void LogInfoMessage(const std::string&) override {}
  MOCK_METHOD(void, LogErrorMessage, (const std::string&));
  MOCK_METHOD(void, OnSourceChanged, ());
  MOCK_METHOD(void, OnRemotingStateChanged, (bool is_remoting));

  MOCK_METHOD(void, OnGetVideoCaptureHost, ());
  MOCK_METHOD(void, OnGetSocketFactory, ());
  MOCK_METHOD(void, OnCreateAudioStream, ());
  MOCK_METHOD(void, OnConnectToRemotingSource, ());

  // Called when an outbound message is sent.
  MOCK_METHOD(void, OnOutboundMessage, (SenderMessage::Type type));

  MOCK_METHOD(void, OnInitialized, ());

  // mojom::CastMessageChannel implementation (outbound messages).
  void OnMessage(mojom::CastMessagePtr message) override {
    EXPECT_TRUE(message->message_namespace == mojom::kWebRtcNamespace ||
                message->message_namespace == mojom::kRemotingNamespace);

    const Json::Value json_value =
        ParseAsJsoncppValue(message->json_format_data);

    ErrorOr<SenderMessage> parsed_message = SenderMessage::Parse(json_value);
    EXPECT_TRUE(parsed_message);
    last_sent_offer_ = parsed_message.value();
    if (parsed_message.value().type == SenderMessage::Type::kOffer) {
      EXPECT_GT(parsed_message.value().sequence_number, 0);
      const auto offer = std::get<Offer>(parsed_message.value().body);

      for (const openscreen::cast::AudioStream& stream : offer.audio_streams) {
        EXPECT_EQ(
            base::Milliseconds(
                std::chrono::milliseconds(stream.stream.target_delay).count()),
            target_playout_delay_);
      }
      for (const VideoStream& stream : offer.video_streams) {
        EXPECT_EQ(
            base::Milliseconds(
                std::chrono::milliseconds(stream.stream.target_delay).count()),
            target_playout_delay_);
      }
    } else if (parsed_message.value().type ==
               SenderMessage::Type::kGetCapabilities) {
      EXPECT_GT(parsed_message.value().sequence_number, 0);
    }

    OnOutboundMessage(parsed_message.value().type);
    if (run_loop_quit_closure_) {
      std::move(run_loop_quit_closure_).Run();
    }
  }

  // mojom::ResourceProvider overrides.
  void BindGpu(mojo::PendingReceiver<viz::mojom::Gpu> receiver) override {}
  void GetVideoCaptureHost(
      mojo::PendingReceiver<media::mojom::VideoCaptureHost> receiver) override {
    video_host_ =
        std::make_unique<NiceMock<FakeVideoCaptureHost>>(std::move(receiver));
    OnGetVideoCaptureHost();
  }

  void GetVideoEncoderMetricsProvider(
      mojo::PendingReceiver<media::mojom::VideoEncoderMetricsProvider> receiver)
      override {}

  void GetSocketFactory(
      mojo::PendingReceiver<network::mojom::SocketFactory> receiver) override {
    socket_factory_ =
        std::make_unique<NiceMock<MockSocketFactory>>(std::move(receiver));
    OnGetSocketFactory();
  }

  void CreateAudioStream(
      mojo::PendingRemote<mojom::AudioStreamCreatorClient> client,
      const media::AudioParameters& params,
      uint32_t total_segments) override {
    OnCreateAudioStream();
  }

  void ConnectToRemotingSource(
      mojo::PendingRemote<media::mojom::Remoter> remoter,
      mojo::PendingReceiver<media::mojom::RemotingSource> receiver) override {
    remoter_.Bind(std::move(remoter));
    remoter_.reset_on_disconnect();
    remoting_source_.Bind(std::move(receiver));
    remoting_source_.reset_on_disconnect();
    OnConnectToRemotingSource();
  }

  void GenerateAndReplyWithAnswer() {
    ASSERT_TRUE(session_host_);
    ASSERT_TRUE(last_sent_offer_);

    const Offer& offer = std::get<Offer>(last_sent_offer_->body);
    openscreen::cast::Answer answer{.udp_port = 1234};

    if (!offer.audio_streams.empty()) {
      answer.send_indexes.push_back(offer.audio_streams[0].stream.index);
      answer.ssrcs.push_back(next_receiver_ssrc_++);
    }

    if (!offer.video_streams.empty()) {
      answer.send_indexes.push_back(offer.video_streams[0].stream.index);
      answer.ssrcs.push_back(next_receiver_ssrc_++);
    }

    openscreen::cast::ReceiverMessage receiver_message{
        .type = openscreen::cast::ReceiverMessage::Type::kAnswer,
        .sequence_number = last_sent_offer_->sequence_number,
        .valid = true,
        .body = std::move(answer)};
    Json::Value message_json = receiver_message.ToJson().value();
    ErrorOr<std::string> message_string = Stringify(message_json);
    ASSERT_TRUE(message_string);

    mojom::CastMessagePtr message = mojom::CastMessage::New(
        openscreen::cast::kCastWebrtcNamespace, message_string.value());
    inbound_channel_->OnMessage(std::move(message));
  }

  OpenscreenSessionHost::AsyncInitializedCallback MakeOnInitializedCallback() {
    return base::BindOnce(&OpenscreenSessionHostTest::OnInitialized,
                          base::Unretained(this));
  }

  // Create a mirroring session. Expect to send OFFER message.
  void CreateSession(SessionType session_type,
                     bool is_remote_playback = false,
                     bool enable_rtcp_reporting = false) {
    session_type_ = session_type;
    is_remote_playback_ = is_remote_playback;
    mojom::SessionParametersPtr session_params =
        mojom::SessionParameters::New();
    session_params->type = session_type_;
    session_params->receiver_address = receiver_endpoint_.address();
    session_params->receiver_friendly_name = "Chromecast Ultra";
    session_params->source_id = "sender-123";
    session_params->destination_id = "receiver-456";
    if (target_playout_delay_ != kDefaultPlayoutDelay) {
      session_params->target_playout_delay = target_playout_delay_;
    }
    if (force_letterboxing_) {
      session_params->force_letterboxing = true;
    }
    if (enable_rtcp_reporting) {
      session_params->enable_rtcp_reporting = true;
    }
    session_params->is_remote_playback = is_remote_playback_;
    cast_mode_ = "mirroring";
    mojo::PendingRemote<mojom::ResourceProvider> resource_provider_remote;
    mojo::PendingRemote<mojom::SessionObserver> session_observer_remote;
    mojo::PendingRemote<mojom::CastMessageChannel> outbound_channel_remote;
    resource_provider_receiver_.Bind(
        resource_provider_remote.InitWithNewPipeAndPassReceiver());
    session_observer_receiver_.Bind(
        session_observer_remote.InitWithNewPipeAndPassReceiver());
    outbound_channel_receiver_.Bind(
        outbound_channel_remote.InitWithNewPipeAndPassReceiver());
    // Expect to send OFFER message when session is created.
    EXPECT_CALL(*this, OnGetSocketFactory());
    EXPECT_CALL(*this, OnError(_)).Times(0);
    EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer));
    EXPECT_CALL(*this, OnInitialized());
    EXPECT_CALL(*this, OnRemotingStateChanged(false));
    session_host_ = std::make_unique<OpenscreenSessionHost>(
        std::move(session_params), gfx::Size(1920, 1080),
        std::move(session_observer_remote), std::move(resource_provider_remote),
        std::move(outbound_channel_remote),
        inbound_channel_.BindNewPipeAndPassReceiver(), nullptr,
        // NOTE: unretained used is safe since we wait for this task to complete
        // before deleting `this`.
        base::BindOnce(&OpenscreenSessionHostTest::OnSessionHostDeletion,
                       base::Unretained(this)));
    session_host_->AsyncInitialize(MakeOnInitializedCallback());
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
  }

  void ExpectStreamsToStart() {
    const int get_video_host_call_count =
        session_type_ == SessionType::AUDIO_ONLY ? 0 : 1;
    EXPECT_CALL(*this, OnGetVideoCaptureHost())
        .Times(get_video_host_call_count);

    const int create_audio_stream_call_count =
        session_type_ == SessionType::VIDEO_ONLY ? 0 : 1;
    EXPECT_CALL(*this, OnCreateAudioStream())
        .Times(create_audio_stream_call_count);
  }

  // Negotiates a mirroring session.
  void StartSession() {
    ASSERT_EQ(cast_mode_, "mirroring");
    ExpectStreamsToStart();
    EXPECT_CALL(*this, OnError(_)).Times(0);
    if (!is_remote_playback_) {
      EXPECT_CALL(*this,
                  OnOutboundMessage(SenderMessage::Type::kGetCapabilities));
    }
    EXPECT_CALL(*this, DidStart());
    GenerateAndReplyWithAnswer();
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
  }

  // Negotiate mirroring.
  void NegotiateMirroring() { session_host_->NegotiateMirroring(); }

  void DeleteSessionHost() {
    ASSERT_TRUE(session_host_);
    session_host_deletion_cb_ = task_environment_.QuitClosure();
    session_host_.reset();
    task_environment_.RunUntilQuit();
  }

  void StopSession() {
    if (video_host_) {
      EXPECT_CALL(*video_host_, OnStopped());
    }
    EXPECT_CALL(*this, DidStop());
    DeleteSessionHost();
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
  }

  void RemotePlaybackSessionTimeOut() {
    EXPECT_TRUE(video_host_);
    EXPECT_CALL(*video_host_, OnStopped());
    EXPECT_CALL(*this, DidStop());
    task_environment_.AdvanceClock(base::Seconds(5));
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
  }

  void CaptureOneVideoFrame() {
    ASSERT_EQ(cast_mode_, "mirroring");
    ASSERT_TRUE(video_host_);
    // Expect to send out some UDP packets.
    EXPECT_CALL(*socket_factory_->udp_socket(), OnSendTo()).Times(AtLeast(1));
    EXPECT_CALL(*video_host_, ReleaseBuffer(_, _, _));
    // Send one video frame to the consumer.
    video_host_->SendOneFrame(gfx::Size(64, 32), base::TimeTicks::Now());
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(socket_factory_.get());
    Mock::VerifyAndClear(video_host_.get());
  }

  void SignalAnswerTimeout() {
    EXPECT_CALL(*this, LogErrorMessage(_));
    if (cast_mode_ == "mirroring") {
      EXPECT_CALL(*this, DidStop());
      EXPECT_CALL(*this, OnError(SessionError::ANSWER_TIME_OUT));
    } else {
      EXPECT_CALL(*this, DidStop()).Times(0);
      EXPECT_CALL(*this, OnError(SessionError::ANSWER_TIME_OUT)).Times(0);
      // Expect to send OFFER message to fallback on mirroring.
      EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer));
      EXPECT_CALL(*this, OnRemotingStateChanged(false));
      // The start of remoting is expected to fail.
      EXPECT_CALL(
          remoting_source_,
          OnStartFailed(RemotingStartFailReason::INVALID_ANSWER_MESSAGE));
      EXPECT_CALL(remoting_source_, OnSinkGone()).Times(AtLeast(1));
    }

    session_host_->OnError(session_host_->session_.get(),
                           openscreen::Error::Code::kAnswerTimeout);
    task_environment_.RunUntilIdle();
    cast_mode_ = "mirroring";
    Mock::VerifyAndClear(this);
    Mock::VerifyAndClear(&remoting_source_);
  }

  void SignalNoStreamSelected() {
    session_host_->OnError(
        session_host_->session_.get(),
        openscreen::Error(openscreen::Error::Code::kNoStreamSelected));
  }

  void SendRemotingCapabilities() {
    static const openscreen::cast::RemotingCapabilities capabilities{
        {openscreen::cast::AudioCapability::kBaselineSet,
         openscreen::cast::AudioCapability::kAac,
         openscreen::cast::AudioCapability::kOpus},
        {openscreen::cast::VideoCapability::kSupports4k,
         openscreen::cast::VideoCapability::kVp8,
         openscreen::cast::VideoCapability::kVp9,
         openscreen::cast::VideoCapability::kH264,
         openscreen::cast::VideoCapability::kHevc}};

    EXPECT_CALL(*this, OnConnectToRemotingSource());
    EXPECT_CALL(remoting_source_, OnSinkAvailable(_));

    session_host_->OnCapabilitiesDetermined(session_host_->session_.get(),
                                            capabilities);
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
    Mock::VerifyAndClear(&remoting_source_);
  }

  void SendRemotingNotSupported() {
    // Remoting not being supported should NOT surface an error.
    EXPECT_CALL(*this, OnError(_)).Times(0);

    session_host_->OnError(
        session_host_->session_.get(),
        openscreen::Error(openscreen::Error::Code::kRemotingNotSupported,
                          "this receiver does not support remoting"));

    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
  }

  void StartRemoting() {
    base::RunLoop run_loop;
    ASSERT_TRUE(remoter_.is_bound());
    // GET_CAPABILITIES is only sent once at the start of mirroring.
    EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kGetCapabilities))
        .Times(0);
    EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer))
        .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
    EXPECT_CALL(*this, OnRemotingStateChanged(true));
    if (is_remote_playback_) {
      EXPECT_TRUE(video_host_);
      if (video_host_) {
        EXPECT_TRUE(video_host_->paused());
      }
    }
    remoter_->Start();
    run_loop.Run();
    task_environment_.RunUntilIdle();
    cast_mode_ = "remoting";
    Mock::VerifyAndClear(this);
  }

  void RemotingStarted() {
    ASSERT_EQ(cast_mode_, "remoting");
    EXPECT_CALL(remoting_source_, OnStarted());
    GenerateAndReplyWithAnswer();
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
    Mock::VerifyAndClear(&remoting_source_);
  }

  void StopRemotingAndRestartMirroring() {
    ASSERT_EQ(cast_mode_, "remoting");
    const RemotingStopReason reason = RemotingStopReason::LOCAL_PLAYBACK;
    // Expect to send OFFER message to fallback on mirroring.
    EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer));
    EXPECT_CALL(*this, OnRemotingStateChanged(false));
    EXPECT_CALL(remoting_source_, OnStopped(reason));
    remoter_->Stop(reason);
    task_environment_.RunUntilIdle();
    cast_mode_ = "mirroring";
    Mock::VerifyAndClear(this);
    Mock::VerifyAndClear(&remoting_source_);
  }

  void StopRemotingAndStopSession() {
    ASSERT_EQ(cast_mode_, "remoting");
    const RemotingStopReason reason = RemotingStopReason::LOCAL_PLAYBACK;
    EXPECT_CALL(remoting_source_, OnStopped(reason));
    if (video_host_) {
      EXPECT_CALL(*video_host_, OnStopped());
    }
    EXPECT_CALL(*this, DidStop());
    remoter_->Stop(reason);
    task_environment_.RunUntilIdle();
    Mock::VerifyAndClear(this);
    Mock::VerifyAndClear(&remoting_source_);
  }

  void SwitchSourceTab() {
    ExpectStreamsToStart();
    EXPECT_CALL(*video_host_, OnStopped());
    EXPECT_CALL(*this, OnConnectToRemotingSource());
    EXPECT_CALL(*this, OnSourceChanged());

    if (cast_mode_ == "remoting") {
      EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer));
      EXPECT_CALL(*this, OnError(_)).Times(0);
      // GET_CAPABILITIES is only sent once at the start of mirroring.
      EXPECT_CALL(*this,
                  OnOutboundMessage(SenderMessage::Type::kGetCapabilities))
          .Times(0);
      const RemotingStopReason reason = RemotingStopReason::LOCAL_PLAYBACK;
      EXPECT_CALL(remoting_source_, OnStopped(reason));
    }

    ASSERT_TRUE(session_host_);
    session_host_->SwitchSourceTab();
    task_environment_.RunUntilIdle();

    // Offer/Answer calls are unnecessary when switching from mirroring to
    // mirroring.
    if (cast_mode_ != "mirroring") {
      cast_mode_ = "mirroring";
      GenerateAndReplyWithAnswer();
      task_environment_.RunUntilIdle();
    }

    Mock::VerifyAndClear(this);
    Mock::VerifyAndClear(&remoting_source_);
  }

  void PushNonFatalAudioCaptureError() {
    ASSERT_TRUE(session_host_->state_ !=
                OpenscreenSessionHost::State::kStopped);

    FrameSenderConfig config;
    config.audio_codec_params =
        media::cast::AudioCodecParams{media::AudioCodec::kOpus};

    PushAudioEncoderStatus(
        config, media::cast::OperationalStatus::STATUS_CODEC_REINIT_PENDING);

    // An ignored status should not stop the session or pause capture.
    ASSERT_NE(session_host_->state_, OpenscreenSessionHost::State::kStopped);
  }

  void PushFatalAudioCaptureError() {
    ASSERT_TRUE(session_host_->state_ !=
                OpenscreenSessionHost::State::kStopped);

    FrameSenderConfig config;
    config.audio_codec_params =
        media::cast::AudioCodecParams{media::AudioCodec::kOpus};

    PushAudioEncoderStatus(
        config, media::cast::OperationalStatus::STATUS_UNSUPPORTED_CODEC);

    // A fatal encoder status update should stop the session.
    ASSERT_EQ(session_host_->state_, OpenscreenSessionHost::State::kStopped);
  }

  void RestartVideoEncoderDueToError() {
    ASSERT_TRUE(session_host_->state_ !=
                OpenscreenSessionHost::State::kStopped);

    // Uh oh! The encoder has a pending reinitialization.
    FrameSenderConfig config;
    config.use_hardware_encoder = false;
    config.video_codec_params =
        media::cast::VideoCodecParams{media::VideoCodec::kVP8};

    PushVideoEncoderStatus(
        config, media::cast::OperationalStatus::STATUS_CODEC_REINIT_PENDING);

    // A pending reinitialization should not stop the session entirely.
    ASSERT_NE(session_host_->state_, OpenscreenSessionHost::State::kStopped);

    // Then restart the encoder.
    PushVideoEncoderStatus(config,
                           media::cast::OperationalStatus::STATUS_INITIALIZED);

    // The session should be still going and the video encoder should be
    // started.
    ASSERT_NE(session_host_->state_, OpenscreenSessionHost::State::kStopped);
  }

  void SetTargetPlayoutDelay(int target_playout_delay_ms) {
    target_playout_delay_ = base::Milliseconds(target_playout_delay_ms);
  }

  void ForceLetterboxing() { force_letterboxing_ = true; }

  void SetAnswer(std::unique_ptr<openscreen::cast::Answer> answer) {
    answer_ = std::move(answer);
  }

  OpenscreenSessionHost& session_host() { return *session_host_; }

  const openscreen::cast::SenderMessage& last_sent_offer() const {
    EXPECT_TRUE(last_sent_offer_);
    return *last_sent_offer_;
  }

  void set_run_loop_quit_closure(base::OnceClosure closure) {
    run_loop_quit_closure_ = std::move(closure);
  }

  base::test::TaskEnvironment& task_environment() { return task_environment_; }

  void PushAudioEncoderStatus(const media::cast::FrameSenderConfig& config,
                              media::cast::OperationalStatus status) {
    session_host_->OnAudioEncoderStatus(config, status);
  }

  void PushVideoEncoderStatus(const media::cast::FrameSenderConfig& config,
                              media::cast::OperationalStatus status) {
    session_host_->OnVideoEncoderStatus(config, status);
  }

  const std::vector<media::cast::FrameSenderConfig>& LastOfferedVideoConfigs() {
    return session_host_->last_offered_video_configs_;
  }

  void SetSupportedProfiles(
      media::VideoEncodeAccelerator::SupportedProfiles profiles) {
    session_host_->supported_profiles_ = std::move(profiles);
  }

  void AssertCodecWasOffered(media::VideoCodec codec,
                             bool use_hardware_encoder = false) {
    // First check that is was actually included in the offer.
    const auto& offer = std::get<Offer>(last_sent_offer().body);
    ASSERT_TRUE(std::any_of(
        offer.video_streams.begin(), offer.video_streams.end(),
        [codec](const VideoStream& stream) {
          return stream.codec == media::cast::ToOpenscreenVideoCodec(codec);
        }));

    // Ensure that we recorded it as a last offered video config, with the right
    // selection of hardware or software encoding.
    ASSERT_TRUE(std::any_of(
        LastOfferedVideoConfigs().begin(), LastOfferedVideoConfigs().end(),
        [codec,
         use_hardware_encoder](const media::cast::FrameSenderConfig& config) {
          return config.video_codec() == codec &&
                 config.use_hardware_encoder == use_hardware_encoder;
        }));
  }

  void AssertCodecWasNotOffered(media::VideoCodec codec) {
    const auto& offer = std::get<Offer>(last_sent_offer().body);
    ASSERT_FALSE(std::any_of(
        offer.video_streams.begin(), offer.video_streams.end(),
        [codec](const VideoStream& stream) {
          return stream.codec == media::cast::ToOpenscreenVideoCodec(codec);
        }));

    ASSERT_FALSE(std::any_of(
        LastOfferedVideoConfigs().begin(), LastOfferedVideoConfigs().end(),
        [codec](const media::cast::FrameSenderConfig& config) {
          return config.video_codec() == codec;
        }));
  }

 protected:
  std::unique_ptr<FakeVideoCaptureHost> video_host_;

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  NiceMock<MockRemotingSource> remoting_source_;
  std::unique_ptr<OpenscreenSessionHost> session_host_;

 private:
  const net::IPEndPoint receiver_endpoint_ = GetFreeLocalPort();
  mojo::Receiver<mojom::ResourceProvider> resource_provider_receiver_{this};
  mojo::Receiver<mojom::SessionObserver> session_observer_receiver_{this};
  mojo::Receiver<mojom::CastMessageChannel> outbound_channel_receiver_{this};
  mojo::Remote<mojom::CastMessageChannel> inbound_channel_;
  SessionType session_type_ = SessionType::AUDIO_AND_VIDEO;
  bool is_remote_playback_ = false;
  mojo::Remote<media::mojom::Remoter> remoter_;
  std::string cast_mode_;
  base::TimeDelta target_playout_delay_{kDefaultPlayoutDelay};
  bool force_letterboxing_{false};

  base::OnceClosure session_host_deletion_cb_;
  std::unique_ptr<MockSocketFactory> socket_factory_;
  std::unique_ptr<openscreen::cast::Answer> answer_;

  int next_receiver_ssrc_{35336};
  std::optional<openscreen::cast::SenderMessage> last_sent_offer_;
  base::OnceClosure run_loop_quit_closure_;
};

TEST_F(OpenscreenSessionHostTest, AudioOnlyMirroring) {
  CreateSession(SessionType::AUDIO_ONLY);
  StartSession();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, VideoOnlyMirroring) {
  SetTargetPlayoutDelay(1000);
  CreateSession(SessionType::VIDEO_ONLY);
  StartSession();
  CaptureOneVideoFrame();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, AudioAndVideoMirroring) {
  SetTargetPlayoutDelay(150);
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  StopSession();
}

// TODO(crbug.com/1363512): Remove support for sender side letterboxing.
TEST_F(OpenscreenSessionHostTest, AnswerWithConstraints) {
  SetAnswer(std::make_unique<openscreen::cast::Answer>(kAnswerWithConstraints));
  media::VideoCaptureParams::SuggestedConstraints expected_constraints = {
      .min_frame_size = gfx::Size(320, 180),
      .max_frame_size = gfx::Size(1920, 1080),
      .fixed_aspect_ratio = true};
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  StopSession();
  EXPECT_EQ(video_host_->GetVideoCaptureParams().SuggestConstraints(),
            expected_constraints);
}

// TODO(crbug.com/1363512): Remove support for sender side letterboxing.
TEST_F(OpenscreenSessionHostTest, AnswerWithConstraintsLetterboxForced) {
  ForceLetterboxing();
  SetAnswer(std::make_unique<openscreen::cast::Answer>(kAnswerWithConstraints));
  media::VideoCaptureParams::SuggestedConstraints expected_constraints = {
      .min_frame_size = gfx::Size(320, 180),
      .max_frame_size = gfx::Size(1920, 1080),
      .fixed_aspect_ratio = true};
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  StopSession();
  EXPECT_EQ(video_host_->GetVideoCaptureParams().SuggestConstraints(),
            expected_constraints);
}

TEST_F(OpenscreenSessionHostTest, AnswerTimeout) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  SignalAnswerTimeout();
}

TEST_F(OpenscreenSessionHostTest, IgnoresNonFatalAudioCaptureErrors) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  PushNonFatalAudioCaptureError();
}

TEST_F(OpenscreenSessionHostTest, StopsSessionOnAudioCaptureError) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  PushFatalAudioCaptureError();
}

TEST_F(OpenscreenSessionHostTest,
       PauseAndUnpauseDueToVideoEncoderStatusChange) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  RestartVideoEncoderDueToError();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, ResumeVideoCapture) {
  CreateSession(SessionType::VIDEO_ONLY);
  StartSession();

  // WE should pause the session twice, and only resume once.
  EXPECT_CALL(*video_host_, OnPaused()).Times(2);
  EXPECT_CALL(*video_host_, OnResumed());

  // Pause video capture.
  PauseCapturingVideo();

  // Resume with the same parameters.
  EXPECT_TRUE(TryResumeCapturingVideo());

  // Pause again.
  PauseCapturingVideo();

  // Change the video capture parameters.
  mirror_settings()->SetMaxResolutionConstraints(gfx::Size(1280, 720));

  // Resume with different parameters.
  EXPECT_FALSE(TryResumeCapturingVideo());

  StopSession();
}

TEST_F(OpenscreenSessionHostTest, SwitchToAndFromRemoting) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  StartRemoting();
  RemotingStarted();
  StopRemotingAndRestartMirroring();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, SwitchFromRemotingForRemotePlayback) {
  CreateSession(SessionType::AUDIO_AND_VIDEO, true);
  StartSession();
  StartRemoting();
  RemotingStarted();
  StopRemotingAndStopSession();
}

TEST_F(OpenscreenSessionHostTest, RemotingNotSupported) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingNotSupported();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, StopSessionWhileRemoting) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  StartRemoting();
  RemotingStarted();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, StartRemotingFailed) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  StartRemoting();
  SignalAnswerTimeout();
  GenerateAndReplyWithAnswer();
  CaptureOneVideoFrame();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, SwitchSourceTabFromMirroring) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  SwitchSourceTab();
  StartRemoting();
  RemotingStarted();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, SwitchSourceTabFromRemoting) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  StartRemoting();
  RemotingStarted();
  SwitchSourceTab();
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, StartRemotePlaybackTimeOut) {
  CreateSession(SessionType::AUDIO_AND_VIDEO, true);
  StartSession();
  RemotePlaybackSessionTimeOut();
}

// TODO(crbug.com/40238532): reenable adaptive playout delay.
TEST_F(OpenscreenSessionHostTest, ChangeTargetPlayoutDelay) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();

  // Currently new delays are ignored due to the playout delay being bounded by
  // the minimum and maximum both being set to the default value.
  session_host().SetTargetPlayoutDelay(base::Milliseconds(300));
  EXPECT_EQ(session_host().audio_sender_->GetTargetPlayoutDelay(),
            kDefaultPlayoutDelay);
  EXPECT_EQ(session_host().audio_sender_->GetTargetPlayoutDelay(),
            kDefaultPlayoutDelay);

  StopSession();
}

TEST_F(OpenscreenSessionHostTest, UpdateBandwidthEstimate) {
  CreateSession(SessionType::VIDEO_ONLY);
  StartSession();

  constexpr uint32_t kMinVideoBitrate = 393216;
  constexpr uint32_t kMaxVideoBitrate = 1250000;
  // Default bitrate should match kDefaultBitrate (5 Mbps).
  EXPECT_EQ(5000000u, session_host().GetVideoNetworkBandwidth());

  // If the estimate is below the minimum, it should stay at the minimum.
  session_host().forced_bandwidth_estimate_for_testing_ = 1000;
  session_host().UpdateBandwidthEstimate();
  EXPECT_EQ(kMinVideoBitrate, session_host().GetVideoNetworkBandwidth());

  // It should gradually reach the max bandwidth estimate when raised.
  session_host().forced_bandwidth_estimate_for_testing_ = 1000000;
  session_host().UpdateBandwidthEstimate();
  EXPECT_EQ(432537u, session_host().GetVideoNetworkBandwidth());

  session_host().UpdateBandwidthEstimate();
  EXPECT_EQ(475790u, session_host().GetVideoNetworkBandwidth());
  for (int i = 0; i < 20; ++i) {
    session_host().UpdateBandwidthEstimate();
  }
  // The max should be 80% of `forced_bandwidth_estimate_for_testing_`.
  EXPECT_EQ(800000u, session_host().GetVideoNetworkBandwidth());

  // The video bitrate should stay saturated at the cap when reached.
  session_host().forced_bandwidth_estimate_for_testing_ = kMaxVideoBitrate + 1;
  for (int i = 0; i < 20; ++i) {
    session_host().UpdateBandwidthEstimate();
  }
  // The max should be 80% of `kMaxVideoBitrate`.
  EXPECT_EQ(1000000u, session_host().GetVideoNetworkBandwidth());

  StopSession();
}

TEST_F(OpenscreenSessionHostTest, CanRequestRefresh) {
  CreateSession(SessionType::VIDEO_ONLY);

  // We just want to make sure this doesn't result in an error or crash.
  session_host().RequestRefreshFrame();
}

TEST_F(OpenscreenSessionHostTest, Vp9CodecEnabledInOffer) {
  base::test::ScopedFeatureList feature_list(media::kCastStreamingVp9);
  CreateSession(SessionType::VIDEO_ONLY);
  AssertCodecWasOffered(media::VideoCodec::kVP9);
}

TEST_F(OpenscreenSessionHostTest, Av1CodecEnabledInOffer) {
// Cast streaming of AV1 is desktop only.
#if !BUILDFLAG(IS_ANDROID) && BUILDFLAG(ENABLE_LIBAOM)
  base::test::ScopedFeatureList feature_list(media::kCastStreamingAv1);
  CreateSession(SessionType::VIDEO_ONLY);
  AssertCodecWasOffered(media::VideoCodec::kAV1);
#endif
}

TEST_F(OpenscreenSessionHostTest, ShouldEnableHardwareVp8EncodingIfSupported) {
  CreateSession(SessionType::VIDEO_ONLY);

  // Mock the profiles to enable VP8 hardware encode.
  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::VP8PROFILE_ANY,
              gfx::Size{1920, 1080})});
  base::RunLoop run_loop;
  set_run_loop_quit_closure(run_loop.QuitClosure());
  NegotiateMirroring();
  run_loop.Run();

  AssertCodecWasOffered(media::VideoCodec::kVP8, true);
}

TEST_F(OpenscreenSessionHostTest, ShouldEnableHardwareAv1EncodingIfSupported) {
  base::test::ScopedFeatureList feature_list(media::kCastStreamingAv1);
  CreateSession(SessionType::VIDEO_ONLY);

  // Mock the profiles to enable AV1 hardware encode.
  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::AV1PROFILE_PROFILE_MAIN,
              gfx::Size{1920, 1080})});
  base::RunLoop run_loop;
  set_run_loop_quit_closure(run_loop.QuitClosure());
  NegotiateMirroring();
  run_loop.Run();

  AssertCodecWasOffered(media::VideoCodec::kAV1, true);
}

TEST_F(OpenscreenSessionHostTest,
       ShouldDisableHardwareAv1EncodingIfSwitchPresent) {
  base::test::ScopedFeatureList feature_list(media::kCastStreamingAv1);
  base::test::ScopedCommandLine scoped_command_line;
  scoped_command_line.GetProcessCommandLine()->AppendSwitch(
      switches::kCastStreamingForceDisableHardwareAv1);
  CreateSession(SessionType::VIDEO_ONLY);

  // Mock the profiles to enable AV1 hardware encode.
  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::AV1PROFILE_PROFILE_MAIN,
              gfx::Size{1920, 1080})});
  base::RunLoop run_loop;
  set_run_loop_quit_closure(run_loop.QuitClosure());
  NegotiateMirroring();
  run_loop.Run();

#if BUILDFLAG(ENABLE_LIBAOM)
  // We should have offered AV1 with hardware DISABLED because of the switch.
  AssertCodecWasOffered(media::VideoCodec::kAV1, false);
#else
  AssertCodecWasNotOffered(media::VideoCodec::kAV1);
#endif
}

TEST_F(OpenscreenSessionHostTest,
       ShouldDisableHardwareEncodingIfEncoderReportsAnIssue) {
  CreateSession(SessionType::VIDEO_ONLY);

  // Mock the profiles to enable VP8 hardware encode.
  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::VP8PROFILE_ANY,
              gfx::Size{1920, 1080})});
  NegotiateMirroring();
  task_environment().RunUntilIdle();

  // We should have offered VP8 with hardware ENABLED.
  AssertCodecWasOffered(media::VideoCodec::kVP8, true);

  // Oh no! The encoder had a problem.
  FrameSenderConfig config;
  config.use_hardware_encoder = true;
  config.video_codec_params =
      media::cast::VideoCodecParams{media::VideoCodec::kVP8};
  PushVideoEncoderStatus(
      config, media::cast::OperationalStatus::STATUS_CODEC_INIT_FAILED);

  // This should have forced a renegotiation with hardware DISABLED.
  AssertCodecWasOffered(media::VideoCodec::kVP8, false);
}

TEST_F(OpenscreenSessionHostTest, ShouldEnableHardwareH264EncodingIfSupported) {
#if BUILDFLAG(IS_WIN)
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(media::kCastStreamingWinHardwareH264);
#endif

  CreateSession(SessionType::VIDEO_ONLY);

  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::H264PROFILE_MIN,
              gfx::Size{1920, 1080})});
  base::RunLoop run_loop;
  set_run_loop_quit_closure(run_loop.QuitClosure());
  NegotiateMirroring();
  run_loop.Run();

  AssertCodecWasOffered(media::VideoCodec::kH264, true);
}

TEST_F(OpenscreenSessionHostTest, ShouldEnableHardwareHevcEncodingIfSupported) {
  base::test::ScopedFeatureList feature_list(media::kCastStreamingHardwareHevc);
  CreateSession(SessionType::VIDEO_ONLY);

  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::HEVCPROFILE_MAIN,
              gfx::Size{1920, 1080})});
  base::RunLoop run_loop;
  set_run_loop_quit_closure(run_loop.QuitClosure());
  NegotiateMirroring();
  run_loop.Run();

  AssertCodecWasOffered(media::VideoCodec::kHEVC, true);
}

TEST_F(OpenscreenSessionHostTest,
       ShouldDisableHardwareHevcEncodingIfSwitchPresent) {
  base::test::ScopedFeatureList feature_list(media::kCastStreamingHardwareHevc);
  base::test::ScopedCommandLine scoped_command_line;
  scoped_command_line.GetProcessCommandLine()->AppendSwitch(
      switches::kCastStreamingForceDisableHardwareHevc);
  CreateSession(SessionType::VIDEO_ONLY);

  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::HEVCPROFILE_MAIN,
              gfx::Size{1920, 1080})});
  base::RunLoop run_loop;
  set_run_loop_quit_closure(run_loop.QuitClosure());
  NegotiateMirroring();
  run_loop.Run();

  AssertCodecWasNotOffered(media::VideoCodec::kHEVC);
}

TEST_F(OpenscreenSessionHostTest, GetStatsDefault) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  EXPECT_TRUE(session_host().GetMirroringStats().empty());
}

TEST_F(OpenscreenSessionHostTest, GetStatsEnabled) {
  CreateSession(SessionType::AUDIO_AND_VIDEO, /* remote_playback */ false,
                /* rtcp_reporting */ true);
  session_host().SetSenderStatsForTest(ConstructDefaultSenderStats());
  EXPECT_FALSE(session_host().GetMirroringStats().empty());
}

TEST_F(OpenscreenSessionHostTest, TwoStageNegotiationFallback) {
  // 1. Force enable VP9, H264 for this test.
  std::vector<base::test::FeatureRef> features = {
      media::kCastStreamingVp9,
      mirroring::features::kCastStreamingOfferHardwareFirst};
#if BUILDFLAG(IS_WIN)
  features.push_back(media::kCastStreamingWinHardwareH264);
#endif
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(features, {});

  // 2. Create the session. (Triggers initial asynchronous offer with no HW
  // profiles).
  CreateSession(SessionType::VIDEO_ONLY);

  // 3. Now set HW profiles to enable HW H264.
  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::H264PROFILE_MIN,
              gfx::Size{1920, 1080})});

  // 4. Manually trigger "Stage 1" negotiation with HW H264 active.
  EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer));
  NegotiateMirroring();
  task_environment().RunUntilIdle();

  // Verify Stage 1 only offered HW H264 (no SW VP9).
  AssertCodecWasOffered(media::VideoCodec::kH264, true);
  const auto& offer1 = std::get<Offer>(last_sent_offer().body);
  ASSERT_FALSE(std::any_of(
      offer1.video_streams.begin(), offer1.video_streams.end(),
      [](const VideoStream& stream) {
        return stream.codec ==
               media::cast::ToOpenscreenVideoCodec(media::VideoCodec::kVP9);
      }));

  // 5. Expect a second OFFER when we trigger the fallback (Stage 2).
  EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer));

  // 6. Trigger the kNoStreamSelected error (receiver rejected H264).
  SignalNoStreamSelected();
  task_environment().RunUntilIdle();

  // 7. Verify that Stage 2 OFFER now contains VP9 SW.
  AssertCodecWasOffered(media::VideoCodec::kVP9, false);
}

TEST_F(OpenscreenSessionHostTest, SingleStageOfferOffersAllSupportedCodecs) {
  // 1. Force enable VP9, H264 for this test.
  std::vector<base::test::FeatureRef> features = {
      media::kCastStreamingVp9,
  };
#if BUILDFLAG(IS_WIN)
  features.push_back(media::kCastStreamingWinHardwareH264);
#endif
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(
      features, {mirroring::features::kCastStreamingOfferHardwareFirst});

  // 2. Create the session. (Triggers initial asynchronous offer with no HW
  // profiles).
  CreateSession(SessionType::VIDEO_ONLY);

  // 3. Now set HW profiles to enable HW H264.
  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::H264PROFILE_MIN,
              gfx::Size{1920, 1080})});

  // 4. Manually trigger "Stage 1" negotiation with HW H264 active.
  EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer));
  NegotiateMirroring();
  task_environment().RunUntilIdle();

  // Verify Stage 1 only offered HW H264 and software VP9;
  AssertCodecWasOffered(media::VideoCodec::kH264, true);
  AssertCodecWasOffered(media::VideoCodec::kVP9, false);
}

TEST_F(OpenscreenSessionHostTest,
       CreateRemotingDataStreamSenderReturnsNullBeforeRemoting) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();

  // Before remoting starts, remoting_stream_data_ is null so
  // CreateRemotingDataStreamSender should return nullptr.
  mojo::ScopedDataPipeProducerHandle producer;
  mojo::ScopedDataPipeConsumerHandle consumer;
  ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(1000, producer, consumer));
  mojo::PendingRemote<media::mojom::RemotingDataStreamSender> pending_sender;
  auto result = session_host().CreateRemotingDataStreamSender(
      /*is_audio=*/true, std::move(consumer),
      pending_sender.InitWithNewPipeAndPassReceiver(), base::DoNothing());
  EXPECT_FALSE(result);

  StopSession();
}

TEST_F(OpenscreenSessionHostTest,
       CreateRemotingDataStreamSenderSucceedsDuringRemoting) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  StartRemoting();
  RemotingStarted();

  // After remoting has started and negotiation completed,
  // CreateRemotingDataStreamSender should return a valid sender.
  mojo::ScopedDataPipeProducerHandle audio_producer;
  mojo::ScopedDataPipeConsumerHandle audio_consumer;
  ASSERT_EQ(MOJO_RESULT_OK,
            mojo::CreateDataPipe(1000, audio_producer, audio_consumer));
  mojo::PendingRemote<media::mojom::RemotingDataStreamSender>
      audio_pending_sender;
  auto audio_result = session_host().CreateRemotingDataStreamSender(
      /*is_audio=*/true, std::move(audio_consumer),
      audio_pending_sender.InitWithNewPipeAndPassReceiver(), base::DoNothing());
  EXPECT_TRUE(audio_result);

  mojo::ScopedDataPipeProducerHandle video_producer;
  mojo::ScopedDataPipeConsumerHandle video_consumer;
  ASSERT_EQ(MOJO_RESULT_OK,
            mojo::CreateDataPipe(1000, video_producer, video_consumer));
  mojo::PendingRemote<media::mojom::RemotingDataStreamSender>
      video_pending_sender;
  auto video_result = session_host().CreateRemotingDataStreamSender(
      /*is_audio=*/false, std::move(video_consumer),
      video_pending_sender.InitWithNewPipeAndPassReceiver(), base::DoNothing());
  EXPECT_TRUE(video_result);

  // RemotingSender objects must be destroyed before StopSession(), since Open
  // Screen requires all Senders to be destroyed before the SenderSession.
  audio_result.reset();
  video_result.reset();

  StopSession();
}

TEST_F(OpenscreenSessionHostTest,
       CreateRemotingDataStreamSenderReturnsNullAfterSenderConsumed) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  StartRemoting();
  RemotingStarted();

  // First call should succeed and move the sender out.
  mojo::ScopedDataPipeProducerHandle producer1;
  mojo::ScopedDataPipeConsumerHandle consumer1;
  ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(1000, producer1, consumer1));
  mojo::PendingRemote<media::mojom::RemotingDataStreamSender> pending1;
  auto result1 = session_host().CreateRemotingDataStreamSender(
      /*is_audio=*/true, std::move(consumer1),
      pending1.InitWithNewPipeAndPassReceiver(), base::DoNothing());
  EXPECT_TRUE(result1);

  // Second call for the same stream should return nullptr since the sender
  // was already moved.
  mojo::ScopedDataPipeProducerHandle producer2;
  mojo::ScopedDataPipeConsumerHandle consumer2;
  ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(1000, producer2, consumer2));
  mojo::PendingRemote<media::mojom::RemotingDataStreamSender> pending2;
  auto result2 = session_host().CreateRemotingDataStreamSender(
      /*is_audio=*/true, std::move(consumer2),
      pending2.InitWithNewPipeAndPassReceiver(), base::DoNothing());
  EXPECT_FALSE(result2);

  // RemotingSender must be destroyed before StopSession().
  result1.reset();

  StopSession();
}

TEST_F(OpenscreenSessionHostTest, CodecParameterInOffer) {
  CreateSession(SessionType::VIDEO_ONLY);

#if BUILDFLAG(IS_WIN)
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(media::kCastStreamingWinHardwareH264);
#endif

  SetSupportedProfiles(
      std::vector<media::VideoEncodeAccelerator::SupportedProfile>{
          media::VideoEncodeAccelerator::SupportedProfile(
              media::VideoCodecProfile::H264PROFILE_HIGH,
              gfx::Size{1920, 1080})});

  base::RunLoop run_loop;
  EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer))
      .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));

  NegotiateMirroring();
  run_loop.Run();

  const auto& offer = std::get<Offer>(last_sent_offer().body);
  ASSERT_FALSE(offer.video_streams.empty());

  bool found_h264 = false;
  for (const auto& stream : offer.video_streams) {
    if (stream.codec == openscreen::cast::VideoCodec::kH264) {
#if BUILDFLAG(IS_MAC)
      if (base::FeatureList::IsEnabled(media::kCastMacForceBaselineProfile)) {
        EXPECT_EQ(stream.stream.codec_parameter, "avc1.420028");
      } else {
        EXPECT_EQ(stream.stream.codec_parameter, "avc1.4d0028");
      }
#else
      EXPECT_EQ(stream.stream.codec_parameter, "avc1.4d0028");
#endif
      found_h264 = true;
    } else if (stream.codec == openscreen::cast::VideoCodec::kVp9) {
      EXPECT_EQ(stream.stream.codec_parameter, "vp09.00.40.08");
    }
  }
  EXPECT_TRUE(found_h264);
  StopSession();
}

TEST_F(OpenscreenSessionHostTest, RemotingNegotiationMismatchedCodec) {
  CreateSession(SessionType::AUDIO_AND_VIDEO);
  StartSession();
  SendRemotingCapabilities();
  StartRemoting();

  // At this point, last_offered_audio_config_ and last_offered_video_configs_
  // contain configs with kUnknown codecs.

  // Simulate negotiation completion with specific codecs (e.g., Opus and VP8).
  openscreen::cast::SessionConfig config(
      1, 2, 48000, 2, std::chrono::milliseconds(400), {}, {});

  openscreen::cast::SenderSession::ConfiguredSenders senders;
  senders.audio_sender = std::make_unique<MockSender>(config);
  senders.audio_config.codec = openscreen::cast::AudioCodec::kOpus;
  senders.video_sender = std::make_unique<MockSender>(config);
  senders.video_config.codec = openscreen::cast::VideoCodec::kVp8;

  // During Media Remoting, it is expected and normal for the negotiated codec
  // to differ from the offered `kUnknown` codec. This should not crash!
  EXPECT_CALL(remoting_source_, OnStarted());
  session_host_->OnNegotiated(nullptr, std::move(senders),
                              openscreen::cast::capture_recommendations::Recommendations{});
  task_environment_.RunUntilIdle();

  StopSession();
}

}  // namespace mirroring
