// Copyright 2017 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/discovery/dial/dial_media_sink_service_impl.h"

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/mock_callback.h"
#include "base/timer/mock_timer.h"
#include "chrome/browser/media/router/discovery/dial/dial_device_data.h"
#include "chrome/browser/media/router/test/provider_test_helpers.h"
#include "content/public/test/browser_task_environment.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::IsEmpty;
using ::testing::Return;

namespace media_router {

class MockDeviceDescriptionService : public DeviceDescriptionService {
 public:
  MockDeviceDescriptionService(DeviceDescriptionParseSuccessCallback success_cb,
                               DeviceDescriptionParseErrorCallback error_cb)
      : DeviceDescriptionService(success_cb, error_cb) {}
  ~MockDeviceDescriptionService() override = default;

  MOCK_METHOD1(GetDeviceDescriptions,
               void(const std::vector<DialDeviceData>& devices));
};

class DialMediaSinkServiceImplTest : public ::testing::Test {
 public:
  DialMediaSinkServiceImplTest()
      : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
        media_sink_service_(new DialMediaSinkServiceImpl(
            mock_sink_discovered_cb_.Get(),
            base::SequencedTaskRunner::GetCurrentDefault())) {}

  void SetUp() override {
    auto mock_description_service =
        std::make_unique<MockDeviceDescriptionService>(mock_success_cb_.Get(),
                                                       mock_error_cb_.Get());
    mock_description_service_ = mock_description_service.get();
    media_sink_service_->SetDescriptionServiceForTest(
        std::move(mock_description_service));

    mock_timer_ = new base::MockOneShotTimer();
    media_sink_service_->SetTimerForTest(base::WrapUnique(mock_timer_.get()));

    auto mock_app_discovery_service =
        std::make_unique<MockDialAppDiscoveryService>();
    mock_app_discovery_service_ = mock_app_discovery_service.get();
    media_sink_service_->SetAppDiscoveryServiceForTest(
        std::move(mock_app_discovery_service));
    base::RunLoop().RunUntilIdle();
  }

  base::CallbackListSubscription StartMonitoringAvailableSinksForApp(
      const std::string& app_name) {
    return media_sink_service_->StartMonitoringAvailableSinksForApp(
        app_name, base::BindRepeating(
                      &DialMediaSinkServiceImplTest::GetAvailableSinksForApp,
                      base::Unretained(this)));
  }

  void GetAvailableSinksForApp(const std::string& app_name) {
    OnSinksAvailableForApp(app_name,
                           media_sink_service_->GetAvailableSinks(app_name));
  }

  MOCK_METHOD2(OnSinksAvailableForApp,
               void(const std::string& app_name,
                    const std::vector<MediaSinkInternal>& available_sinks));

  DialAppInfoResult CreateDialAppInfoResult(const std::string& app_name) {
    return DialAppInfoResult(
        CreateParsedDialAppInfoPtr(app_name, DialAppState::kRunning),
        DialAppInfoResultCode::kOk);
  }

  bool dial_discovery_started() {
    return media_sink_service_->dial_registry_ != nullptr;
  }

 protected:
  const content::BrowserTaskEnvironment task_environment_;
  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;

  base::MockCallback<OnSinksDiscoveredCallback> mock_sink_discovered_cb_;
  base::MockCallback<
      MockDeviceDescriptionService::DeviceDescriptionParseSuccessCallback>
      mock_success_cb_;
  base::MockCallback<
      MockDeviceDescriptionService::DeviceDescriptionParseErrorCallback>
      mock_error_cb_;

  raw_ptr<MockDeviceDescriptionService, DanglingUntriaged>
      mock_description_service_;
  raw_ptr<MockDialAppDiscoveryService, DanglingUntriaged>
      mock_app_discovery_service_;
  raw_ptr<base::MockOneShotTimer, DanglingUntriaged> mock_timer_;

  std::unique_ptr<DialMediaSinkServiceImpl> media_sink_service_;

  MediaSinkInternal dial_sink_1_ = CreateDialSink(1);
  MediaSinkInternal dial_sink_2_ = CreateDialSink(2);
};

TEST_F(DialMediaSinkServiceImplTest, OnDeviceDescriptionAvailable) {
  DialDeviceData device_data("first", GURL("http://127.0.0.1/dd.xml"),
                             base::Time::Now());
  ParsedDialDeviceDescription device_description;
  device_description.model_name = "model name";
  device_description.friendly_name = "friendly name";
  device_description.app_url = GURL("http://192.168.1.1/apps");
  device_description.unique_id = "unique id";

  media_sink_service_->OnDeviceDescriptionAvailable(device_data,
                                                    device_description);
  EXPECT_TRUE(media_sink_service_->GetSinks().empty());

  std::vector<DialDeviceData> device_list = {device_data};
  EXPECT_CALL(*mock_description_service_, GetDeviceDescriptions(device_list));

  media_sink_service_->OnDialDeviceList(device_list);
  media_sink_service_->OnDeviceDescriptionAvailable(device_data,
                                                    device_description);
  EXPECT_TRUE(mock_timer_->IsRunning());
  EXPECT_CALL(mock_sink_discovered_cb_, Run(Not(IsEmpty())));
  mock_timer_->Fire();
  EXPECT_EQ(1u, media_sink_service_->GetSinks().size());
}

TEST_F(DialMediaSinkServiceImplTest,
       OnDeviceDescriptionAvailableIPAddressChanged) {
  DialDeviceData device_data("first", GURL("http://127.0.0.1/dd.xml"),
                             base::Time::Now());
  ParsedDialDeviceDescription device_description;
  device_description.model_name = "model name";
  device_description.friendly_name = "friendly name";
  device_description.app_url = GURL("http://192.168.1.1/apps");
  device_description.unique_id = "unique id";

  std::vector<DialDeviceData> device_list = {device_data};
  EXPECT_CALL(*mock_description_service_, GetDeviceDescriptions(device_list));
  media_sink_service_->OnDialDeviceList(device_list);

  media_sink_service_->OnDeviceDescriptionAvailable(device_data,
                                                    device_description);

  EXPECT_TRUE(mock_timer_->IsRunning());
  EXPECT_CALL(mock_sink_discovered_cb_, Run(_));
  mock_timer_->Fire();
  EXPECT_EQ(1u, media_sink_service_->GetSinks().size());

  device_description.app_url = GURL("http://192.168.1.100/apps");
  media_sink_service_->OnDeviceDescriptionAvailable(device_data,
                                                    device_description);

  EXPECT_TRUE(mock_timer_->IsRunning());
  EXPECT_CALL(mock_sink_discovered_cb_, Run(_));
  mock_timer_->Fire();

  EXPECT_EQ(1u, media_sink_service_->GetSinks().size());
  for (const auto& dial_sink_it : media_sink_service_->GetSinks()) {
    EXPECT_EQ(device_description.app_url,
              dial_sink_it.second.dial_data().app_url);
  }
}

TEST_F(DialMediaSinkServiceImplTest, OnDeviceDescriptionRestartsTimer) {
  DialDeviceData device_data("first", GURL("http://127.0.0.1/dd.xml"),
                             base::Time::Now());
  ParsedDialDeviceDescription device_description;
  device_description.model_name = "model name";
  device_description.friendly_name = "friendly name";
  device_description.app_url = GURL("http://192.168.1.1/apps");
  device_description.unique_id = "unique id";

  std::vector<DialDeviceData> device_list = {device_data};
  EXPECT_CALL(*mock_description_service_, GetDeviceDescriptions(device_list));

  EXPECT_FALSE(mock_timer_->IsRunning());
  media_sink_service_->OnDialDeviceList(device_list);
  media_sink_service_->OnDeviceDescriptionAvailable(device_data,
                                                    device_description);
  EXPECT_TRUE(mock_timer_->IsRunning());

  EXPECT_CALL(mock_sink_discovered_cb_, Run(_));
  mock_timer_->Fire();

  EXPECT_FALSE(mock_timer_->IsRunning());
  device_description.app_url = GURL("http://192.168.1.11/apps");
  media_sink_service_->OnDeviceDescriptionAvailable(device_data,
                                                    device_description);
  EXPECT_TRUE(mock_timer_->IsRunning());
}

TEST_F(DialMediaSinkServiceImplTest, OnDialDeviceListRestartsTimer) {
  EXPECT_CALL(*mock_description_service_, GetDeviceDescriptions(IsEmpty()));
  media_sink_service_->OnDialDeviceList(std::vector<DialDeviceData>());
  EXPECT_TRUE(mock_timer_->IsRunning());

  EXPECT_CALL(mock_sink_discovered_cb_, Run(_)).Times(0);
  mock_timer_->Fire();

  EXPECT_CALL(*mock_description_service_, GetDeviceDescriptions(IsEmpty()));
  media_sink_service_->OnDialDeviceList(std::vector<DialDeviceData>());
  EXPECT_TRUE(mock_timer_->IsRunning());

  EXPECT_CALL(mock_sink_discovered_cb_, Run(_)).Times(0);
  mock_timer_->Fire();
}

TEST_F(DialMediaSinkServiceImplTest, StartStopMonitoringAvailableSinksForApp) {
  const MediaSink::Id& sink_id = dial_sink_1_.sink().id();
  EXPECT_CALL(*mock_app_discovery_service_,
              DoFetchDialAppInfo(sink_id, "YouTube"))
      .Times(1);
  media_sink_service_->AddOrUpdateSink(dial_sink_1_);
  base::CallbackListSubscription subscription1 =
      StartMonitoringAvailableSinksForApp("YouTube");
  base::CallbackListSubscription subscription2 =
      StartMonitoringAvailableSinksForApp("YouTube");
  EXPECT_EQ(1u, media_sink_service_->sink_queries_.size());

  subscription1 = {};
  EXPECT_EQ(1u, media_sink_service_->sink_queries_.size());
  subscription2 = {};
  EXPECT_TRUE(media_sink_service_->sink_queries_.empty());
}

TEST_F(DialMediaSinkServiceImplTest, OnDialAppInfoAvailableNoStartMonitoring) {
  const MediaSink::Id& sink_id = dial_sink_1_.sink().id();

  EXPECT_CALL(*this, OnSinksAvailableForApp(_, _)).Times(0);
  media_sink_service_->AddOrUpdateSink(dial_sink_1_);
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id, "YouTube", CreateDialAppInfoResult("YouTube"));
}

TEST_F(DialMediaSinkServiceImplTest, OnDialAppInfoAvailableNoSink) {
  const MediaSink::Id& sink_id = dial_sink_1_.sink().id();

  EXPECT_CALL(*this, OnSinksAvailableForApp("YouTube", _)).Times(0);
  base::CallbackListSubscription subscription =
      StartMonitoringAvailableSinksForApp("YouTube");
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id, "YouTube", CreateDialAppInfoResult("YouTube"));
}

TEST_F(DialMediaSinkServiceImplTest, OnDialAppInfoAvailableSinksAdded) {
  const MediaSink::Id& sink_id1 = dial_sink_1_.sink().id();
  const MediaSink::Id& sink_id2 = dial_sink_2_.sink().id();

  media_sink_service_->AddOrUpdateSink(dial_sink_1_);
  media_sink_service_->AddOrUpdateSink(dial_sink_2_);

  EXPECT_CALL(*mock_app_discovery_service_,
              DoFetchDialAppInfo(sink_id1, "YouTube"));
  EXPECT_CALL(*mock_app_discovery_service_,
              DoFetchDialAppInfo(sink_id2, "YouTube"));
  EXPECT_CALL(*mock_app_discovery_service_,
              DoFetchDialAppInfo(sink_id1, "Netflix"));
  EXPECT_CALL(*mock_app_discovery_service_,
              DoFetchDialAppInfo(sink_id2, "Netflix"));
  EXPECT_CALL(*this, OnSinksAvailableForApp(_, _)).Times(0);
  base::CallbackListSubscription subscription1 =
      StartMonitoringAvailableSinksForApp("YouTube");
  base::CallbackListSubscription subscription2 =
      StartMonitoringAvailableSinksForApp("Netflix");

  // Either kStopped or kRunning means the app is available on the sink.
  EXPECT_CALL(*this,
              OnSinksAvailableForApp(
                  "YouTube", std::vector<MediaSinkInternal>({dial_sink_1_})));
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id1, "YouTube", CreateDialAppInfoResult("YouTube"));

  EXPECT_CALL(*this, OnSinksAvailableForApp("YouTube",
                                            std::vector<MediaSinkInternal>(
                                                {dial_sink_1_, dial_sink_2_})));
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id2, "YouTube", CreateDialAppInfoResult("YouTube"));

  EXPECT_CALL(*this,
              OnSinksAvailableForApp(
                  "Netflix", std::vector<MediaSinkInternal>({dial_sink_2_})));
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id2, "Netflix", CreateDialAppInfoResult("Netflix"));

  // Stop listening for Netflix.
  subscription2 = {};
  EXPECT_CALL(*this, OnSinksAvailableForApp("Netflix", _)).Times(0);
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id1, "Netflix", CreateDialAppInfoResult("Netflix"));

  std::vector<MediaSinkInternal> expected_sinks = {dial_sink_1_, dial_sink_2_};
  EXPECT_EQ(expected_sinks, media_sink_service_->GetAvailableSinks("YouTube"));
  EXPECT_EQ(expected_sinks, media_sink_service_->GetAvailableSinks("Netflix"));
}

TEST_F(DialMediaSinkServiceImplTest, OnDialAppInfoAvailableSinksRemoved) {
  const MediaSink::Id& sink_id = dial_sink_1_.sink().id();

  EXPECT_CALL(*mock_app_discovery_service_, DoFetchDialAppInfo(_, _));
  media_sink_service_->AddOrUpdateSink(dial_sink_1_);
  base::CallbackListSubscription subscription1 =
      StartMonitoringAvailableSinksForApp("YouTube");

  EXPECT_CALL(*this,
              OnSinksAvailableForApp(
                  "YouTube", std::vector<MediaSinkInternal>({dial_sink_1_})));
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id, "YouTube", CreateDialAppInfoResult("YouTube"));

  EXPECT_CALL(*this, OnSinksAvailableForApp("YouTube", IsEmpty()));
  media_sink_service_->RemoveSink(dial_sink_1_);
  media_sink_service_->OnDiscoveryComplete();
}

TEST_F(DialMediaSinkServiceImplTest,
       OnDialAppInfoAvailableWithAlreadyAvailableSinks) {
  const MediaSink::Id& sink_id = dial_sink_1_.sink().id();

  EXPECT_CALL(*mock_app_discovery_service_, DoFetchDialAppInfo(_, _));
  media_sink_service_->AddOrUpdateSink(dial_sink_1_);
  base::CallbackListSubscription subscription1 =
      StartMonitoringAvailableSinksForApp("YouTube");

  EXPECT_CALL(*this,
              OnSinksAvailableForApp(
                  "YouTube", std::vector<MediaSinkInternal>({dial_sink_1_})))
      .Times(1);
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id, "YouTube", CreateDialAppInfoResult("YouTube"));
  media_sink_service_->OnAppInfoParseCompleted(
      sink_id, "YouTube", CreateDialAppInfoResult("YouTube"));
}

TEST_F(DialMediaSinkServiceImplTest, StartAfterStopMonitoringForApp) {
  EXPECT_CALL(*mock_app_discovery_service_, DoFetchDialAppInfo(_, _));
  media_sink_service_->AddOrUpdateSink(dial_sink_1_);
  base::CallbackListSubscription subscription1 =
      StartMonitoringAvailableSinksForApp("YouTube");
  std::vector<MediaSinkInternal> expected_sinks = {dial_sink_1_};

  EXPECT_CALL(*this, OnSinksAvailableForApp("YouTube", expected_sinks))
      .Times(1);
  media_sink_service_->OnAppInfoParseCompleted(
      dial_sink_1_.sink().id(), "YouTube", CreateDialAppInfoResult("YouTube"));

  subscription1 = {};

  EXPECT_EQ(expected_sinks, media_sink_service_->GetAvailableSinks("YouTube"));
  base::CallbackListSubscription subscription2 =
      StartMonitoringAvailableSinksForApp("YouTube");
  EXPECT_EQ(expected_sinks, media_sink_service_->GetAvailableSinks("YouTube"));
}

TEST_F(DialMediaSinkServiceImplTest, FetchDialAppInfoWithDiscoveryOnlySink) {
  media_router::DialSinkExtraData extra_data = dial_sink_1_.dial_data();
  extra_data.model_name = "Eureka Dongle";
  dial_sink_1_.set_dial_data(extra_data);

  EXPECT_CALL(*mock_app_discovery_service_, DoFetchDialAppInfo(_, _)).Times(0);
  media_sink_service_->AddOrUpdateSink(dial_sink_1_);
  base::CallbackListSubscription subscription1 =
      StartMonitoringAvailableSinksForApp("YouTube");
}

class DialMediaSinkServiceImplStartDiscoveryTest
    : public DialMediaSinkServiceImplTest {
  // Override this function so that `media_sink_service_` isn't initialized for
  // tests yet.
  void SetUp() override {}
};

TEST_F(DialMediaSinkServiceImplStartDiscoveryTest, DiscoveryOnUserGesture) {
  media_sink_service_->Initialize();
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(dial_discovery_started());

  // Calling `DiscoverSinksNow()` won't start a new cycle of discovery.
  media_sink_service_->DiscoverSinksNow();
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(dial_discovery_started());

  media_sink_service_->StartDiscovery();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(dial_discovery_started());
}
}  // namespace media_router
