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

#include "chrome/browser/media/router/mojo/media_router_desktop.h"

#include <stddef.h>
#include <stdint.h>

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

#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/string_escape.h"
#include "base/memory/ref_counted.h"
#include "base/notimplemented.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/media/router/mojo/media_router_mojo_metrics.h"
#include "chrome/browser/media/router/test/media_router_mojo_test.h"
#include "chrome/browser/media/router/test/provider_test_helpers.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/media_router/browser/presentation_connection_message_observer.h"
#include "components/media_router/browser/route_message_util.h"
#include "components/media_router/browser/test/mock_media_router.h"
#include "components/media_router/common/issue.h"
#include "components/media_router/common/media_route.h"
#include "components/media_router/common/media_source.h"
#include "components/media_router/common/test/test_helper.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

using blink::mojom::PresentationConnectionCloseReason;
using blink::mojom::PresentationConnectionState;
using media_router::mojom::RouteMessagePtr;
using testing::_;
using testing::Eq;
using testing::Invoke;
using testing::InvokeWithoutArgs;
using testing::IsEmpty;
using testing::Mock;
using testing::NiceMock;
using testing::Not;
using testing::Return;
using testing::SaveArg;
using testing::Sequence;
using testing::SizeIs;
using testing::StrictMock;
using testing::UnorderedElementsAre;
using testing::WithArg;

namespace media_router {

namespace {

const char kDescription[] = "description";
const char kError[] = "error";
const char kSource[] = "source1";
const char kSource2[] = "source2";
const char kTabSourceOne[] = "urn:x-org.chromium.media:source:tab:1";
const char kTabSourceTwo[] = "urn:x-org.chromium.media:source:tab:2";
const char kRouteId[] = "routeId";
const char kRouteId2[] = "routeId2";
const char kSinkId[] = "sink";
const char kSinkId2[] = "sink2";
const char kSinkName[] = "sinkName";
const char kPresentationId[] = "presentationId";
const char kOrigin[] = "http://origin/";
const int kInvalidFrameNodeId = -1;
const int kTimeoutMillis = 5 * 1000;

IssueInfo CreateIssueInfo(const std::string& title) {
  IssueInfo issue_info;
  issue_info.title = title;
  issue_info.message = std::string("msg");
  issue_info.severity = IssueInfo::Severity::WARNING;
  return issue_info;
}

// Creates a media route whose ID is |kRouteId|.
MediaRoute CreateMediaRoute() {
  MediaRoute route(kRouteId, MediaSource(kSource), kSinkId, kDescription, true);
  route.set_presentation_id(kPresentationId);
  route.set_controller_type(RouteControllerType::kGeneric);
  return route;
}

// Creates a media route whose ID is |kRouteId2|.
MediaRoute CreateMediaRoute2() {
  MediaRoute route(kRouteId2, MediaSource(kSource), kSinkId, kDescription,
                   true);
  route.set_presentation_id(kPresentationId);
  route.set_controller_type(RouteControllerType::kGeneric);
  return route;
}

std::string RouteMessageToString(const RouteMessagePtr& message) {
  if (!message->message && !message->data) {
    return "null";
  }
  std::string result;
  if (message->message) {
    result = "text=";
    base::EscapeJSONString(message->message.value(), true, &result);
  } else {
    const std::string_view src(
        reinterpret_cast<const char*>(message->data.value().data()),
        message->data.value().size());
    result = "binary=" + base::Base64Encode(src);
  }
  return result;
}

std::vector<MediaSinkInternal> ToInternalSinks(
    const std::vector<MediaSink>& sinks) {
  std::vector<MediaSinkInternal> internal_sinks;
  internal_sinks.reserve(sinks.size());
  for (const auto& sink : sinks) {
    MediaSinkInternal internal_sink;
    internal_sink.set_sink(sink);
    internal_sinks.emplace_back(std::move(internal_sink));
  }
  return internal_sinks;
}

class StubMediaRouterDesktop : public MediaRouterDesktop {
 public:
  explicit StubMediaRouterDesktop(content::BrowserContext* context)
      : MediaRouterDesktop(context) {}
  ~StubMediaRouterDesktop() override = default;

  // media_router::MediaRouter: implementation
  base::DictValue GetState() const override {
    NOTIMPLEMENTED();
    return base::DictValue();
  }

  void GetProviderState(
      mojom::MediaRouteProviderId provider_id,
      mojom::MediaRouteProvider::GetStateCallback callback) const override {
    NOTIMPLEMENTED();
  }
};

}  // namespace

class MediaRouterDesktopTest : public MediaRouterMojoTest {
 public:
  MediaRouterDesktopTest() = default;
  ~MediaRouterDesktopTest() override = default;

 protected:
  void ExpectCastResultBucketCount(const std::string& operation,
                                   mojom::RouteRequestResultCode result_code,
                                   int expected_count) {
    ExpectBucketCount("MediaRouter.Provider." + operation + ".Result.Cast",
                      result_code, expected_count);
  }

  void ExpectResultBucketCount(const std::string& operation,
                               mojom::RouteRequestResultCode result_code,
                               int expected_count) {
    ExpectBucketCount("MediaRouter.Provider." + operation + ".Result",
                      result_code, expected_count);
  }

  void JoinRoute(const MediaSource::Id& source_id,
                 const std::string& presentation_id,
                 const url::Origin& origin,
                 content::WebContents* web_contents,
                 MediaRouteResponseCallback callback,
                 base::TimeDelta timeout) {
    router()->JoinRoute(source_id, presentation_id, origin, web_contents,
                        std::move(callback), timeout);
  }

  std::unique_ptr<MediaRouterDesktop> CreateMediaRouter() override {
    auto router = std::unique_ptr<MediaRouterDesktop>(
        new StubMediaRouterDesktop(profile()));
    router->DisableMediaRouteProvidersForTest();
    return router;
  }

  void ReceiveSinks(mojom::MediaRouteProviderId provider_id,
                    const std::string& media_source,
                    const std::vector<MediaSinkInternal>& sinks) {
    router()->OnSinksReceived(
        provider_id, media_source, sinks,
        std::vector<url::Origin>{1, url::Origin::Create(GURL(kOrigin))});
  }

  void ReceiveRouteMessages(const std::string& route_id,
                            std::vector<mojom::RouteMessagePtr> messages) {
    router()->OnRouteMessagesReceived(route_id, std::move(messages));
  }

  void RegisterMediaRoutesObserver(MediaRoutesObserver* observer) {
    router()->RegisterMediaRoutesObserver(observer);
  }

  void UnregisterMediaRoutesObserver(MediaRoutesObserver* observer) {
    router()->UnregisterMediaRoutesObserver(observer);
  }

  void UpdateRoutes(mojom::MediaRouteProviderId provider_id,
                    const std::vector<MediaRoute>& routes) {
    router()->OnRoutesUpdated(provider_id, routes);
  }

  void RecordPresentationRequestUrlBySink(
      const media_router::MediaSource& source,
      media_router::mojom::MediaRouteProviderId provider_id,
      int times) {
    for (int i = 0; i < times; i++) {
      MediaRouterDesktop::RecordPresentationRequestUrlBySink(source,
                                                             provider_id);
    }
  }

  void OnUserGesture() { router()->OnUserGesture(); }

  void SetMockSinkServiceForTest(MockDualMediaSinkService* sink_service) {
    router()->media_sink_service_ = sink_service;
  }

 private:
  void ExpectBucketCount(const std::string& histogram_name,
                         mojom::RouteRequestResultCode result_code,
                         int expected_count) {
    histogram_tester_.ExpectBucketCount(histogram_name, result_code,
                                        expected_count);
  }

  base::HistogramTester histogram_tester_;
};

TEST_F(MediaRouterDesktopTest, CreateRoute) {
  TestCreateRoute();
  ExpectCastResultBucketCount("CreateRoute", mojom::RouteRequestResultCode::OK,
                              1);
}

// Tests that MediaRouter is aware when a route is created, even if
// MediaRouteProvider doesn't call OnRoutesUpdated().
TEST_F(MediaRouterDesktopTest, RouteRecognizedAfterCreation) {
  MockMediaRoutesObserver routes_observer1(router());
  MockMediaRoutesObserver routes_observer2(router());

  EXPECT_CALL(routes_observer2, OnRoutesUpdated(SizeIs(0)));
  EXPECT_CALL(routes_observer2, OnRoutesUpdated(SizeIs(1)));

  // TestCreateRoute() does not explicitly call OnRoutesUpdated() on the router.
  TestCreateRoute();
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, CreateRouteFails) {
  ProvideTestSink(mojom::MediaRouteProviderId::CAST, kSinkId);
  EXPECT_CALL(mock_cast_provider_,
              CreateRouteInternal(kSource, kSinkId, _,
                                  url::Origin::Create(GURL(kOrigin)),
                                  kInvalidFrameNodeId, _, _))
      .WillOnce(
          WithArg<6>([](mojom::MediaRouteProvider::CreateRouteCallback& cb) {
            std::move(cb).Run(std::nullopt, nullptr, std::string(kError),
                              mojom::RouteRequestResultCode::TIMED_OUT);
          }));

  RouteResponseCallbackHandler handler;
  base::RunLoop run_loop;
  EXPECT_CALL(handler, DoInvoke(nullptr, "", kError,
                                mojom::RouteRequestResultCode::TIMED_OUT, _))
      .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
  router()->CreateRoute(kSource, kSinkId, url::Origin::Create(GURL(kOrigin)),
                        nullptr,
                        base::BindOnce(&RouteResponseCallbackHandler::Invoke,
                                       base::Unretained(&handler)),
                        base::Milliseconds(kTimeoutMillis));
  run_loop.Run();
  ExpectCastResultBucketCount("CreateRoute",
                              mojom::RouteRequestResultCode::TIMED_OUT, 1);
}

TEST_F(MediaRouterDesktopTest, JoinRoute) {
  TestJoinRoute(kPresentationId);
  ExpectCastResultBucketCount("JoinRoute", mojom::RouteRequestResultCode::OK,
                              1);
}

TEST_F(MediaRouterDesktopTest, JoinRouteNotFoundFails) {
  RouteResponseCallbackHandler handler;
  base::RunLoop run_loop;
  EXPECT_CALL(handler,
              DoInvoke(nullptr, "", "Route not found",
                       mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, _))
      .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
  JoinRoute(kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)),
            nullptr,
            base::BindOnce(&RouteResponseCallbackHandler::Invoke,
                           base::Unretained(&handler)),
            base::Milliseconds(kTimeoutMillis));
  run_loop.Run();
  ExpectResultBucketCount("JoinRoute",
                          mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, 1);
}

TEST_F(MediaRouterDesktopTest, JoinRouteTimedOutFails) {
  // Make sure the MR has received an update with the route, so it knows there
  // is a route to join.
  const std::vector<MediaRoute> routes{CreateMediaRoute()};
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes);

  EXPECT_CALL(mock_cast_provider_,
              JoinRouteInternal(
                  kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)),
                  kInvalidFrameNodeId, base::Milliseconds(kTimeoutMillis), _))
      .WillOnce(
          WithArg<5>([](mojom::MediaRouteProvider::JoinRouteCallback& cb) {
            std::move(cb).Run(std::nullopt, nullptr, std::string(kError),
                              mojom::RouteRequestResultCode::TIMED_OUT);
          }));

  RouteResponseCallbackHandler handler;
  base::RunLoop run_loop;
  EXPECT_CALL(handler, DoInvoke(nullptr, "", kError,
                                mojom::RouteRequestResultCode::TIMED_OUT, _))
      .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
  JoinRoute(kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)),
            nullptr,
            base::BindOnce(&RouteResponseCallbackHandler::Invoke,
                           base::Unretained(&handler)),
            base::Milliseconds(kTimeoutMillis));
  run_loop.Run();
  ExpectCastResultBucketCount("JoinRoute",
                              mojom::RouteRequestResultCode::TIMED_OUT, 1);
}

TEST_F(MediaRouterDesktopTest, JoinRoute_EscalationFromTabToDesktopFails) {
  MediaRoute route(kRouteId, MediaSource(kTabSourceOne), kSinkId, kDescription,
                   true);
  route.set_presentation_id(kPresentationId);
  route.set_controller_type(RouteControllerType::kGeneric);

  const std::vector<MediaRoute> routes{route};
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes);

  const std::string desktop_source =
      "urn:x-org.chromium.media:source:desktop:screen:0:0";
  RouteResponseCallbackHandler handler;
  base::RunLoop run_loop;

  EXPECT_CALL(handler,
              DoInvoke(nullptr, "",
                       "Cannot switch to desktop capture without user consent",
                       mojom::RouteRequestResultCode::USER_NOT_ALLOWED, _))
      .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));

  JoinRoute(desktop_source, kPresentationId, url::Origin::Create(GURL(kOrigin)),
            nullptr,
            base::BindOnce(&RouteResponseCallbackHandler::Invoke,
                           base::Unretained(&handler)),
            base::Milliseconds(kTimeoutMillis));
  run_loop.Run();
}

TEST_F(MediaRouterDesktopTest, JoinRoute_AutoJoinEscalationFails) {
  MediaRoute route(kRouteId, MediaSource(kTabSourceOne), kSinkId, kDescription,
                   true);
  route.set_presentation_id(kPresentationId);
  route.set_controller_type(RouteControllerType::kGeneric);

  const std::vector<MediaRoute> routes{route};
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes);

  const std::string desktop_source =
      "urn:x-org.chromium.media:source:desktop:screen:0:0";
  RouteResponseCallbackHandler handler;
  base::RunLoop run_loop;

  EXPECT_CALL(handler,
              DoInvoke(nullptr, "",
                       "Cannot switch to desktop capture without user consent",
                       mojom::RouteRequestResultCode::USER_NOT_ALLOWED, _))
      .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));

  JoinRoute(desktop_source, kAutoJoinPresentationId,
            url::Origin::Create(GURL(kOrigin)), nullptr,
            base::BindOnce(&RouteResponseCallbackHandler::Invoke,
                           base::Unretained(&handler)),
            base::Milliseconds(kTimeoutMillis));
  run_loop.Run();
}

TEST_F(MediaRouterDesktopTest, DetachRoute) {
  TestDetachRoute();
}

TEST_F(MediaRouterDesktopTest, TerminateRoute) {
  TestTerminateRoute();
  ExpectCastResultBucketCount("TerminateRoute",
                              mojom::RouteRequestResultCode::OK, 1);
}

TEST_F(MediaRouterDesktopTest, TerminateRouteFails) {
  ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);
  EXPECT_CALL(mock_cast_provider_, TerminateRouteInternal(kRouteId, _))
      .WillOnce([](const std::string& route_id,
                   mojom::MediaRouteProvider::TerminateRouteCallback& cb) {
        std::move(cb).Run(std::string("timed out"),
                          mojom::RouteRequestResultCode::TIMED_OUT);
      });
  router()->TerminateRoute(kRouteId);
  base::RunLoop().RunUntilIdle();
  ExpectCastResultBucketCount("TerminateRoute",
                              mojom::RouteRequestResultCode::OK, 0);
  ExpectCastResultBucketCount("TerminateRoute",
                              mojom::RouteRequestResultCode::TIMED_OUT, 1);
}

#if !BUILDFLAG(IS_ANDROID)
TEST_F(MediaRouterDesktopTest, HandleIssue) {
  MockIssuesObserver issue_observer1(router()->GetIssueManager());
  MockIssuesObserver issue_observer2(router()->GetIssueManager());
  issue_observer1.Init();
  issue_observer2.Init();

  IssueInfo issue_info = CreateIssueInfo("title 1");

  Issue issue_from_observer1(Issue::CreateIssueWithIssueInfo(IssueInfo()));
  Issue issue_from_observer2(Issue::CreateIssueWithIssueInfo(IssueInfo()));
  EXPECT_CALL(issue_observer1, OnIssue(_))
      .WillOnce(SaveArg<0>(&issue_from_observer1));
  EXPECT_CALL(issue_observer2, OnIssue(_))
      .WillOnce(SaveArg<0>(&issue_from_observer2));

  router()->OnIssue(issue_info);
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(issue_from_observer1.id(), issue_from_observer2.id());
  EXPECT_EQ(issue_info, issue_from_observer1.info());
  EXPECT_EQ(issue_info, issue_from_observer2.info());
}

TEST_F(MediaRouterDesktopTest, HandlePermissionIssue) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(
      media_router::kShowCastPermissionRejectedError);

  MockIssuesObserver issue_observer(router()->GetIssueManager());
  issue_observer.Init();
  base::RunLoop run_loop;

  EXPECT_CALL(issue_observer, OnIssue(_))
      .WillOnce([&run_loop](const Issue& received_issue) {
        EXPECT_TRUE(received_issue.is_permission_rejected_issue());
        run_loop.QuitClosure().Run();
      });

  router()->OnLocalDiscoveryPermissionRejected();
  run_loop.Run();
}
#endif  // !BUILDFLAG(IS_ANDROID)

TEST_F(MediaRouterDesktopTest, RegisterAndUnregisterMediaSinksObserver) {
  MediaSource media_source(kSource);

  // These should only be called once even if there is more than one observer
  // for a given source.
  EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kSource));
  EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kSource2));

  auto sinks_observer = std::make_unique<NiceMock<MockMediaSinksObserver>>(
      router(), media_source, url::Origin::Create(GURL(kOrigin)));
  EXPECT_TRUE(sinks_observer->Init());
  auto extra_sinks_observer =
      std::make_unique<NiceMock<MockMediaSinksObserver>>(
          router(), media_source, url::Origin::Create(GURL(kOrigin)));
  EXPECT_TRUE(extra_sinks_observer->Init());
  auto unrelated_sinks_observer =
      std::make_unique<NiceMock<MockMediaSinksObserver>>(
          router(), MediaSource(kSource2), url::Origin::Create(GURL(kOrigin)));
  EXPECT_TRUE(unrelated_sinks_observer->Init());
  base::RunLoop().RunUntilIdle();

  const std::vector<MediaSink> kExpectedSinks{
      CreateCastSink(kSinkId, kSinkName), CreateCastSink(kSinkId2, kSinkName)};

  EXPECT_CALL(*sinks_observer, OnSinksReceived(kExpectedSinks));
  EXPECT_CALL(*extra_sinks_observer, OnSinksReceived(kExpectedSinks));
  ReceiveSinks(mojom::MediaRouteProviderId::CAST, media_source.id(),
               ToInternalSinks(kExpectedSinks));

  // Since the MediaRouterDesktop has already received results for
  // |media_source|, return cached results to observers that are subsequently
  // registered.
  auto cached_sinks_observer = std::make_unique<MockMediaSinksObserver>(
      router(), media_source, url::Origin::Create(GURL(kOrigin)));
  EXPECT_CALL(*cached_sinks_observer, OnSinksReceived(kExpectedSinks));
  EXPECT_TRUE(cached_sinks_observer->Init());

  // Different origin from cached result. Empty list will be returned.
  auto cached_sinks_observer2 = std::make_unique<MockMediaSinksObserver>(
      router(), media_source, url::Origin::Create(GURL("https://youtube.com")));
  EXPECT_CALL(*cached_sinks_observer2, OnSinksReceived(IsEmpty()));
  EXPECT_TRUE(cached_sinks_observer2->Init());

  EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kSource));
  EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kSource2));
  sinks_observer.reset();
  extra_sinks_observer.reset();
  unrelated_sinks_observer.reset();
  cached_sinks_observer.reset();
  cached_sinks_observer2.reset();
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, TabSinksObserverIsShared) {
  MediaSource tab_source_one(kTabSourceOne);
  MediaSource tab_source_two(kTabSourceTwo);

  // Media router should not try to start observing for each tab, but instead
  // only once for all tabs.
  EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kTabSourceOne))
      .Times(0);
  EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kTabSourceTwo))
      .Times(0);
  EXPECT_CALL(mock_cast_provider_,
              StartObservingMediaSinks(MediaSource::ForAnyTab().id()))
      .Times(1);

  const auto origin = url::Origin::Create(GURL(kOrigin));
  auto sinks_observer = std::make_unique<NiceMock<MockMediaSinksObserver>>(
      router(), tab_source_one, origin);
  EXPECT_TRUE(sinks_observer->Init());
  auto extra_sinks_observer =
      std::make_unique<NiceMock<MockMediaSinksObserver>>(
          router(), tab_source_one, origin);
  EXPECT_TRUE(extra_sinks_observer->Init());
  auto second_tab_sinks_observer =
      std::make_unique<NiceMock<MockMediaSinksObserver>>(
          router(), MediaSource(kTabSourceTwo), origin);
  EXPECT_TRUE(second_tab_sinks_observer->Init());
  base::RunLoop().RunUntilIdle();

  const std::vector<MediaSink> kExpectedSinks{
      CreateCastSink(kSinkId, kSinkName), CreateCastSink(kSinkId2, kSinkName)};

  // All tabs should get the same updates.
  EXPECT_CALL(*sinks_observer, OnSinksReceived(kExpectedSinks));
  EXPECT_CALL(*extra_sinks_observer, OnSinksReceived(kExpectedSinks));
  EXPECT_CALL(*second_tab_sinks_observer, OnSinksReceived(kExpectedSinks));
  ReceiveSinks(mojom::MediaRouteProviderId::CAST, tab_source_one.id(),
               ToInternalSinks(kExpectedSinks));

  // Since the MediaRouterDesktop has already received results for
  // |media_source|, return cached results to observers that are subsequently
  // registered.
  auto cached_sinks_observer = std::make_unique<MockMediaSinksObserver>(
      router(), tab_source_one, url::Origin::Create(GURL(kOrigin)));
  EXPECT_CALL(*cached_sinks_observer, OnSinksReceived(kExpectedSinks));
  EXPECT_TRUE(cached_sinks_observer->Init());

  // Different origin from cached result. Empty list will be returned.
  auto cached_sinks_observer2 = std::make_unique<MockMediaSinksObserver>(
      router(), tab_source_one,
      url::Origin::Create(GURL("https://youtube.com")));
  EXPECT_CALL(*cached_sinks_observer2, OnSinksReceived(IsEmpty()));
  EXPECT_TRUE(cached_sinks_observer2->Init());

  // Since tabs share observation, stop observing should not be called.
  EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kTabSourceOne))
      .Times(0);
  EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kTabSourceTwo))
      .Times(0);
  EXPECT_CALL(mock_cast_provider_,
              StopObservingMediaSinks(MediaSource::ForAnyTab().id()))
      .Times(0);
  sinks_observer.reset();
  extra_sinks_observer.reset();
  second_tab_sinks_observer.reset();
  cached_sinks_observer.reset();
  cached_sinks_observer2.reset();
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, RegisterAndUnregisterMediaRoutesObserver) {
  MockMediaRouter mock_router;

  auto routes_observer =
      std::make_unique<MockMediaRoutesObserver>(&mock_router);
  EXPECT_TRUE(
      mock_router.routes_observers().HasObserver(routes_observer.get()));

  auto extra_routes_observer =
      std::make_unique<MockMediaRoutesObserver>(&mock_router);
  EXPECT_TRUE(
      mock_router.routes_observers().HasObserver(extra_routes_observer.get()));

  routes_observer.reset();
  extra_routes_observer.reset();
  EXPECT_TRUE(mock_router.routes_observers().empty());
}

TEST_F(MediaRouterDesktopTest, UnregisterBeforeNotificationDoesntCrash) {
  auto routes_observer = std::make_unique<MediaRoutesObserver>(router());
  auto routes_observer_two = std::make_unique<MediaRoutesObserver>(router());

  // Resetting the observer immediately should cause it to be invalidated before
  // the callback for NotifyOfExistingRoutesIfRegistered is called.
  routes_observer_two.reset();

  // Make sure the Notify task executes.
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, RegisteredObserversGetMediaRouteUpdates) {
  auto routes_observer =
      std::make_unique<StrictMock<MockMediaRoutesObserver>>(router());
  auto extra_routes_observer =
      std::make_unique<StrictMock<MockMediaRoutesObserver>>(router());

  MediaSource media_source(kSource);
  std::vector<MediaRoute> expected_routes{
      MediaRoute(kRouteId, media_source, kSinkId, kDescription, false)};

  EXPECT_CALL(*routes_observer, OnRoutesUpdated(expected_routes)).Times(1);
  EXPECT_CALL(*extra_routes_observer, OnRoutesUpdated(expected_routes))
      .Times(1);

  UpdateRoutes(mojom::MediaRouteProviderId::CAST, expected_routes);

  routes_observer.reset();
  extra_routes_observer.reset();

  // No route observers should be notified.
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, expected_routes);
}

TEST_F(MediaRouterDesktopTest, SendRouteMessage) {
  TestSendRouteMessage();
}

TEST_F(MediaRouterDesktopTest, SendRouteBinaryMessage) {
  TestSendRouteBinaryMessage();
}

namespace {

// Used in the RouteMessages* tests to populate the messages that will be
// processed and dispatched to PresentationConnectionMessageObservers.
void PopulateRouteMessages(std::vector<RouteMessagePtr>* batch1,
                           std::vector<RouteMessagePtr>* batch2,
                           std::vector<RouteMessagePtr>* batch3,
                           std::vector<RouteMessagePtr>* all_messages) {
  batch1->clear();
  batch2->clear();
  batch3->clear();
  batch1->emplace_back(message_util::RouteMessageFromString("text1"));
  batch2->emplace_back(
      message_util::RouteMessageFromData(std::vector<uint8_t>({UINT8_C(1)})));
  batch2->emplace_back(message_util::RouteMessageFromString("text2"));
  batch3->emplace_back(message_util::RouteMessageFromString("text3"));
  batch3->emplace_back(
      message_util::RouteMessageFromData(std::vector<uint8_t>({UINT8_C(2)})));
  batch3->emplace_back(
      message_util::RouteMessageFromData(std::vector<uint8_t>({UINT8_C(3)})));
  all_messages->clear();
  for (auto* message_batch : {batch1, batch2, batch3}) {
    for (auto& message : *message_batch) {
      all_messages->emplace_back(message->Clone());
    }
  }
}

// Used in the RouteMessages* tests to observe and sanity-check that the
// messages being received from the router are correct and in-sequence. The
// checks here correspond to the expected messages in PopulateRouteMessages()
// above.
class ExpectedMessagesObserver final
    : public PresentationConnectionMessageObserver {
 public:
  ExpectedMessagesObserver(MediaRouter* router,
                           const MediaRoute::Id& route_id,
                           std::vector<RouteMessagePtr> expected_messages)
      : PresentationConnectionMessageObserver(router, route_id),
        expected_messages_(std::move(expected_messages)) {}

  ~ExpectedMessagesObserver() override { CheckReceivedMessages(); }

 private:
  void OnMessagesReceived(std::vector<RouteMessagePtr> messages) override {
    for (auto& message : messages) {
      messages_.emplace_back(std::move(message));
    }
  }

  void CheckReceivedMessages() {
    ASSERT_EQ(expected_messages_.size(), messages_.size());
    for (size_t i = 0; i < expected_messages_.size(); i++) {
      EXPECT_EQ(expected_messages_[i], messages_[i])
          << "Message mismatch at index " << i
          << ": expected: " << RouteMessageToString(expected_messages_[i])
          << ", actual: " << RouteMessageToString(messages_[i]);
    }
  }

  std::vector<RouteMessagePtr> expected_messages_;
  std::vector<RouteMessagePtr> messages_;
};

}  // namespace

TEST_F(MediaRouterDesktopTest, RouteMessagesSingleObserver) {
  std::vector<RouteMessagePtr> incoming_batch1, incoming_batch2,
      incoming_batch3, all_messages;
  ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);
  PopulateRouteMessages(&incoming_batch1, &incoming_batch2, &incoming_batch3,
                        &all_messages);

  // Creating ExpectedMessagesObserver will register itself to the
  // MediaRouter, which in turn will start listening for route messages.
  ExpectedMessagesObserver observer(router(), kRouteId,
                                    std::move(all_messages));
  ReceiveRouteMessages(kRouteId, std::move(incoming_batch1));
  ReceiveRouteMessages(kRouteId, std::move(incoming_batch2));
  ReceiveRouteMessages(kRouteId, std::move(incoming_batch3));
  // When |observer| goes out-of-scope, its destructor will ensure all expected
  // messages have been received.
}

TEST_F(MediaRouterDesktopTest, RouteMessagesMultipleObservers) {
  std::vector<RouteMessagePtr> incoming_batch1, incoming_batch2,
      incoming_batch3, all_messages;
  ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);
  PopulateRouteMessages(&incoming_batch1, &incoming_batch2, &incoming_batch3,
                        &all_messages);
  std::vector<RouteMessagePtr> all_messages2;
  for (auto& message : all_messages) {
    all_messages2.emplace_back(message->Clone());
  }

  // The ExpectedMessagesObservers will register themselves with the
  // MediaRouter, which in turn will start listening for route messages.
  ExpectedMessagesObserver observer1(router(), kRouteId,
                                     std::move(all_messages));
  ExpectedMessagesObserver observer2(router(), kRouteId,
                                     std::move(all_messages2));
  ReceiveRouteMessages(kRouteId, std::move(incoming_batch1));
  ReceiveRouteMessages(kRouteId, std::move(incoming_batch2));
  ReceiveRouteMessages(kRouteId, std::move(incoming_batch3));
  // As each |observer| goes out-of-scope, its destructor will ensure all
  // expected messages have been received.
}

TEST_F(MediaRouterDesktopTest, PresentationConnectionStateChangedCallback) {
  MediaRoute::Id route_id("route-id");
  const GURL presentation_url("http://www.example.com/presentation.html");
  const std::string presentation_id("pid");
  blink::mojom::PresentationInfo connection(presentation_url, presentation_id);
  base::MockCallback<content::PresentationConnectionStateChangedCallback>
      callback;
  base::CallbackListSubscription subscription =
      router()->AddPresentationConnectionStateChangedCallback(route_id,
                                                              callback.Get());

  {
    base::RunLoop run_loop;
    content::PresentationConnectionStateChangeInfo closed_info(
        PresentationConnectionState::CLOSED);
    closed_info.close_reason = PresentationConnectionCloseReason::WENT_AWAY;
    closed_info.message = "Foo";

    EXPECT_CALL(callback, Run(StateChangeInfoEquals(closed_info)))
        .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
    router()->OnPresentationConnectionClosed(
        route_id, PresentationConnectionCloseReason::WENT_AWAY, "Foo");
    run_loop.Run();
    EXPECT_TRUE(Mock::VerifyAndClearExpectations(&callback));
  }

  content::PresentationConnectionStateChangeInfo terminated_info(
      PresentationConnectionState::TERMINATED);
  {
    base::RunLoop run_loop;
    EXPECT_CALL(callback, Run(StateChangeInfoEquals(terminated_info)))
        .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
    router()->OnPresentationConnectionStateChanged(
        route_id, PresentationConnectionState::TERMINATED);
    run_loop.Run();

    EXPECT_TRUE(Mock::VerifyAndClearExpectations(&callback));
  }
}

TEST_F(MediaRouterDesktopTest,
       PresentationConnectionStateChangedCallbackRemoved) {
  MediaRoute::Id route_id("route-id");
  base::MockCallback<content::PresentationConnectionStateChangedCallback>
      callback;
  base::CallbackListSubscription subscription =
      router()->AddPresentationConnectionStateChangedCallback(route_id,
                                                              callback.Get());

  // Callback has been removed, so we don't expect it to be called anymore.
  subscription = {};
  EXPECT_TRUE(router()->presentation_connection_state_callbacks_.empty());

  EXPECT_CALL(callback, Run(_)).Times(0);
  router()->OnPresentationConnectionStateChanged(
      route_id, PresentationConnectionState::TERMINATED);
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, GetMediaController) {
  MockMediaController mock_controller;
  mojo::Remote<mojom::MediaController> controller_remote;
  mojo::PendingRemote<mojom::MediaStatusObserver> observer_remote;
  MockMediaStatusObserver mock_observer(
      observer_remote.InitWithNewPipeAndPassReceiver());
  mojo::Remote<mojom::MediaStatusObserver> observer_remote_held_by_controller;
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, {CreateMediaRoute()});

  EXPECT_CALL(mock_cast_provider_,
              BindMediaControllerInternal(kRouteId, _, _, _))
      .WillOnce(
          [&](const std::string& route_id,
              mojo::PendingReceiver<mojom::MediaController>& media_controller,
              mojo::PendingRemote<mojom::MediaStatusObserver>& observer,
              MockMediaRouteProvider::BindMediaControllerCallback& callback) {
            mock_controller.Bind(std::move(media_controller));
            observer_remote_held_by_controller.Bind(std::move(observer));
            std::move(callback).Run(true);
          });
  router()->GetMediaController(kRouteId,
                               controller_remote.BindNewPipeAndPassReceiver(),
                               std::move(observer_remote));
  base::RunLoop().RunUntilIdle();

  EXPECT_CALL(mock_controller, Play());
  controller_remote->Play();
  EXPECT_CALL(mock_observer, OnMediaStatusUpdated(_));
  observer_remote_held_by_controller->OnMediaStatusUpdated(
      mojom::MediaStatus::New());
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, SendSinkRequestsToMultipleProviders) {
  // Register |mock_wired_display_provider_| with the router.
  // |mock_cast_provider_| has already been registered during setup.
  RegisterWiredDisplayProvider();

  // Set |mock_cast_provider_| to have one sink with ID |kSinkId|.
  ProvideTestSink(mojom::MediaRouteProviderId::CAST, kSinkId);
  mock_cast_provider_.SetRouteToReturn(CreateMediaRoute());

  // Set |mock_wired_display_provider_| to have one sink with ID |kSinkId2|.
  ProvideTestSink(mojom::MediaRouteProviderId::WIRED_DISPLAY, kSinkId2);
  mock_wired_display_provider_.SetRouteToReturn(CreateMediaRoute2());

  // A request to |kSinkId| should only be sent to |mock_cast_provider_|.
  EXPECT_CALL(mock_cast_provider_,
              CreateRouteInternal(_, kSinkId, _, _, _, _, _))
      .WillOnce(WithArg<6>(Invoke(
          &mock_cast_provider_, &MockMediaRouteProvider::RouteRequestSuccess)));
  EXPECT_CALL(mock_wired_display_provider_,
              CreateRouteInternal(_, kSinkId, _, _, _, _, _))
      .Times(0);
  router()->CreateRoute(kSource, kSinkId, url::Origin::Create(GURL(kOrigin)),
                        nullptr, base::DoNothing(),
                        base::Milliseconds(kTimeoutMillis));

  // A request to |kSinkId2| should only be sent to
  // |mock_wired_display_provider_|.
  EXPECT_CALL(mock_cast_provider_,
              CreateRouteInternal(_, kSinkId2, _, _, _, _, _))
      .Times(0);
  EXPECT_CALL(mock_wired_display_provider_,
              CreateRouteInternal(_, kSinkId2, _, _, _, _, _))
      .WillOnce(
          WithArg<6>(Invoke(&mock_wired_display_provider_,
                            &MockMediaRouteProvider::RouteRequestSuccess)));
  router()->CreateRoute(kSource, kSinkId2, url::Origin::Create(GURL(kOrigin)),
                        nullptr, base::DoNothing(),
                        base::Milliseconds(kTimeoutMillis));
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, SendRouteRequestsToMultipleProviders) {
  // Register |mock_wired_display_provider_| with the router.
  // |mock_cast_provider_| has already been registered during setup.
  RegisterWiredDisplayProvider();

  // Set |mock_cast_provider_| to have one route with ID |kRouteId|.
  ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);

  // Set |mock_wired_display_provider_| to have one route with ID |kRouteId2|.
  ProvideTestRoute(mojom::MediaRouteProviderId::WIRED_DISPLAY, kRouteId2);

  // A request for |kRouteId| should only be sent to |mock_cast_provider_|.
  EXPECT_CALL(mock_cast_provider_, DetachRoute(kRouteId));
  EXPECT_CALL(mock_wired_display_provider_, DetachRoute(kRouteId)).Times(0);
  router()->DetachRoute(kRouteId);

  // A request for |kRouteId2| should only be sent to
  // |mock_wired_display_provider_|.
  EXPECT_CALL(mock_cast_provider_, TerminateRouteInternal(kRouteId2, _))
      .Times(0);
  EXPECT_CALL(mock_wired_display_provider_,
              TerminateRouteInternal(kRouteId2, _))
      .WillOnce(
          WithArg<1>(Invoke(&mock_wired_display_provider_,
                            &MockMediaRouteProvider::TerminateRouteSuccess)));
  router()->TerminateRoute(kRouteId2);
  base::RunLoop().RunUntilIdle();
}

TEST_F(MediaRouterDesktopTest, ObserveSinksFromMultipleProviders) {
  const MediaSource source(kSource);
  // Sinks for the Cast MRP.
  MediaSinkInternal sink1a;
  MediaSinkInternal sink1b;
  sink1a.set_sink(MediaSink("sink1a", "", SinkIconType::CAST,
                            mojom::MediaRouteProviderId::CAST));
  sink1b.set_sink(MediaSink("sink1b", "", SinkIconType::CAST,
                            mojom::MediaRouteProviderId::CAST));
  // Sinks for the wired display MRP.
  MediaSinkInternal sink2a;
  MediaSinkInternal sink2b;
  sink2a.set_sink(MediaSink("sink2a", "", SinkIconType::CAST,
                            mojom::MediaRouteProviderId::WIRED_DISPLAY));
  sink2b.set_sink(MediaSink("sink2b", "", SinkIconType::CAST,
                            mojom::MediaRouteProviderId::WIRED_DISPLAY));
  RegisterWiredDisplayProvider();
  NiceMock<MockMediaSinksObserver> observer(router(), source,
                                            url::Origin::Create(GURL(kOrigin)));
  observer.Init();

  // Have the Cast MRP report sinks.
  EXPECT_CALL(observer, OnSinksReceived(UnorderedElementsAre(sink1a.sink(),
                                                             sink1b.sink())));
  ReceiveSinks(mojom::MediaRouteProviderId::CAST, kSource, {sink1a, sink1b});

  // Have the wired display MRP report sinks.
  EXPECT_CALL(observer,
              OnSinksReceived(UnorderedElementsAre(
                  sink1a.sink(), sink1b.sink(), sink2a.sink(), sink2b.sink())));
  ReceiveSinks(mojom::MediaRouteProviderId::WIRED_DISPLAY, kSource,
               {sink2a, sink2b});

  // Have the Cast MRP report an empty list of sinks.
  EXPECT_CALL(observer, OnSinksReceived(UnorderedElementsAre(sink2a.sink(),
                                                             sink2b.sink())));
  ReceiveSinks(mojom::MediaRouteProviderId::CAST, kSource, {});

  // Have the wired display MRP report an empty list of sinks.
  EXPECT_CALL(observer, OnSinksReceived(IsEmpty()));
  ReceiveSinks(mojom::MediaRouteProviderId::WIRED_DISPLAY, kSource, {});
}

TEST_F(MediaRouterDesktopTest, ObserveRoutesFromMultipleProviders) {
  const MediaSource source(kSource);
  // Routes for the Cast MRP.
  const MediaRoute route1a("route1a", source, "sink 1a", "", true);
  const MediaRoute route1b("route1b", source, "sink 1b", "", true);
  // Routes for the wired display MRP.
  const MediaRoute route2a("route2a", source, "sink 2a", "", true);
  const MediaRoute route2b("route2b", source, "sink 2b", "", true);
  RegisterWiredDisplayProvider();
  MockMediaRoutesObserver observer(router());

  // Have the Cast MRP report routes.
  EXPECT_CALL(observer,
              OnRoutesUpdated(UnorderedElementsAre(route1a, route1b)));
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, {route1a, route1b});

  // Have the wired display MRP report routes.
  EXPECT_CALL(observer, OnRoutesUpdated(UnorderedElementsAre(
                            route1a, route1b, route2a, route2b)));
  UpdateRoutes(mojom::MediaRouteProviderId::WIRED_DISPLAY, {route2a, route2b});

  // Have the Cast MRP report an empty list of routes.
  EXPECT_CALL(observer,
              OnRoutesUpdated(UnorderedElementsAre(route2a, route2b)));
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, {});

  // Have the wired display MRP report an empty list of routes.
  EXPECT_CALL(observer, OnRoutesUpdated(IsEmpty()));
  UpdateRoutes(mojom::MediaRouteProviderId::WIRED_DISPLAY, {});
}

TEST_F(MediaRouterDesktopTest, TestRecordPresentationRequestUrlBySink) {
  using base::Bucket;
  using PresentationUrlBySink = MediaRouterDesktop::PresentationUrlBySink;

  MediaSource cast_source("cast:ABCD1234");
  MediaSource remote_playback_source(
      "remote-playback:media-session?&video_codec=vp8&audio_codec=aac");
  MediaSource dial_source(
      GURL(base::StrCat({kCastDialPresentationUrlScheme, ":YouTube"})));
  MediaSource presentation_url(GURL("https://www.example.com"));

  base::HistogramTester tester;
  RecordPresentationRequestUrlBySink(cast_source,
                                     mojom::MediaRouteProviderId::CAST, 6);
  RecordPresentationRequestUrlBySink(remote_playback_source,
                                     mojom::MediaRouteProviderId::CAST, 5);
  RecordPresentationRequestUrlBySink(dial_source,
                                     mojom::MediaRouteProviderId::DIAL, 4);
  RecordPresentationRequestUrlBySink(presentation_url,
                                     mojom::MediaRouteProviderId::CAST, 3);
  RecordPresentationRequestUrlBySink(
      presentation_url, mojom::MediaRouteProviderId::WIRED_DISPLAY, 2);
  // DIAL devices don't support normal URLs, so this will get logged as
  // kUnknown.
  RecordPresentationRequestUrlBySink(presentation_url,
                                     mojom::MediaRouteProviderId::DIAL, 1);

  EXPECT_THAT(
      tester.GetAllSamples("MediaRouter.PresentationRequest.UrlBySink2"),
      testing::UnorderedElementsAre(
          Bucket(static_cast<int>(PresentationUrlBySink::kUnknown), 1),
          Bucket(
              static_cast<int>(PresentationUrlBySink::kNormalUrlToWiredDisplay),
              2),
          Bucket(
              static_cast<int>(PresentationUrlBySink::kNormalUrlToChromecast),
              3),
          Bucket(static_cast<int>(PresentationUrlBySink::kDialUrlToDial), 4),
          Bucket(static_cast<int>(PresentationUrlBySink::kRemotePlayback), 5),
          Bucket(static_cast<int>(PresentationUrlBySink::kCastUrlToChromecast),
                 6)));
}

TEST_F(MediaRouterDesktopTest, TestGetCurrentRoutes) {
  MediaSource source1("source_1");
  MediaSource source2("source_1");
  MediaRoute route1("route_1", source1, "sink_1", "", false);
  MediaRoute route2("route_2", source2, "sink_2", "", true);
  std::vector<MediaRoute> routes = {route1, route2};

  EXPECT_TRUE(router()->GetCurrentRoutes().empty());
  router()->current_routes_ = routes;
  std::vector<MediaRoute> current_routes = router()->GetCurrentRoutes();
  ASSERT_EQ(current_routes.size(), 2u);
  EXPECT_EQ(current_routes[0], route1);
  EXPECT_EQ(current_routes[1], route2);

  router()->current_routes_ = std::vector<MediaRoute>();
  EXPECT_TRUE(router()->GetCurrentRoutes().empty());
}

TEST_F(MediaRouterDesktopTest, GetMirroringMediaControllerHost) {
  MediaSource tab_source(kTabSourceOne);
  auto local_mirroring_route =
      MediaRoute(kRouteId, tab_source, kSinkId, kDescription, true);
  local_mirroring_route.set_controller_type(RouteControllerType::kGeneric);
  std::vector<MediaRoute> local_mirroring_routes{local_mirroring_route};
  EXPECT_CALL(mock_cast_provider_,
              BindMediaControllerInternal(kRouteId, _, _, _))
      .WillOnce(
          [&](const std::string& route_id,
              mojo::PendingReceiver<mojom::MediaController>& media_controller,
              mojo::PendingRemote<mojom::MediaStatusObserver>& observer,
              MockMediaRouteProvider::BindMediaControllerCallback& callback) {
            std::move(callback).Run(true);
          });
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, local_mirroring_routes);
  base::RunLoop().RunUntilIdle();

  // Expect the host to exist for a local mirroring source.
  EXPECT_NE(nullptr, router()->GetMirroringMediaControllerHost(kRouteId));

  std::vector<MediaRoute> nonlocal_mirroring_routes{
      MediaRoute(kRouteId2, tab_source, kSinkId, kDescription, false)};
  UpdateRoutes(mojom::MediaRouteProviderId::CAST, nonlocal_mirroring_routes);

  // Expect that the host for kRouteId no longer exists.
  EXPECT_EQ(nullptr, router()->GetMirroringMediaControllerHost(kRouteId));

  // Expect that no host for kRouteId2 exists, as it is not a local source.
  EXPECT_EQ(nullptr, router()->GetMirroringMediaControllerHost(kRouteId2));
}

TEST_F(MediaRouterDesktopTest, OnUserGesture) {
  auto media_sink_service = std::make_unique<MockDualMediaSinkService>();
  SetMockSinkServiceForTest(media_sink_service.get());

  // If Cast and DIAL discovery hasn't started yet, user gesture will start
  // discovery.
  EXPECT_CALL(*media_sink_service, StartDiscovery);
  EXPECT_CALL(*media_sink_service, DiscoverSinksNow).Times(0);
  OnUserGesture();

  // If Cast and DIAL discovery has started, user gesture will not lead to
  // starting discovery again. Instead, the sink service will request to start a
  // new discovery cycle.
  ON_CALL(*media_sink_service, MdnsDiscoveryStarted)
      .WillByDefault(Return(true));
  ON_CALL(*media_sink_service, DialDiscoveryStarted)
      .WillByDefault(Return(true));

  EXPECT_CALL(*media_sink_service, StartDiscovery).Times(0);
  EXPECT_CALL(*media_sink_service, DiscoverSinksNow);
  OnUserGesture();

  // Clean up
  SetMockSinkServiceForTest(nullptr);
}

}  // namespace media_router
