// Copyright 2019 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/system_media_controls/linux/system_media_controls_linux.h"

#include <memory>
#include <utility>

#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "components/system_media_controls/system_media_controls_observer.h"
#include "dbus/message.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_exported_object.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
using ::testing::Unused;
using ::testing::WithArg;

namespace system_media_controls {

namespace internal {

namespace {

constexpr uint32_t kFakeSerial = 123;
const char kTestProductName[] = "Foo";

}  // anonymous namespace

class MockSystemMediaControlsObserver : public SystemMediaControlsObserver {
 public:
  MockSystemMediaControlsObserver() = default;
  ~MockSystemMediaControlsObserver() override = default;

  // SystemMediaControlsObserver implementation.
  MOCK_METHOD0(OnServiceReady, void());
  MOCK_METHOD1(OnNext, void(system_media_controls::SystemMediaControls*));
  MOCK_METHOD1(OnPrevious, void(system_media_controls::SystemMediaControls*));
  MOCK_METHOD1(OnPause, void(system_media_controls::SystemMediaControls*));
  MOCK_METHOD1(OnPlayPause, void(system_media_controls::SystemMediaControls*));
  MOCK_METHOD1(OnStop, void(system_media_controls::SystemMediaControls*));
  MOCK_METHOD1(OnPlay, void(system_media_controls::SystemMediaControls*));
  MOCK_METHOD2(OnSeek,
               void(system_media_controls::SystemMediaControls*,
                    const base::TimeDelta&));
  MOCK_METHOD2(OnSeekTo,
               void(system_media_controls::SystemMediaControls*,
                    const base::TimeDelta&));
};

class SystemMediaControlsLinuxTest : public testing::Test,
                                     public SystemMediaControlsObserver {
 public:
  SystemMediaControlsLinuxTest()
      : task_environment_(base::test::TaskEnvironment::MainThreadType::UI,
                          base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

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

  ~SystemMediaControlsLinuxTest() override = default;

  void SetUp() override { StartMprisServiceAndWaitForReady(); }

  void AddObserver(MockSystemMediaControlsObserver* observer) {
    service_->AddObserver(observer);
  }

  void CallMediaPlayer2PlayerMethodAndBlock(const std::string& method_name) {
    EXPECT_TRUE(player_interface_exported_methods_.contains(method_name));

    response_wait_loop_ = std::make_unique<base::RunLoop>();

    // We need to supply a serial or the test will crash.
    dbus::MethodCall method_call(kMprisAPIPlayerInterfaceName, method_name);
    method_call.SetSerial(kFakeSerial);

    // Call the method and await a response.
    player_interface_exported_methods_[method_name].Run(
        &method_call,
        base::BindRepeating(&SystemMediaControlsLinuxTest::OnResponse,
                            base::Unretained(this)));
    response_wait_loop_->Run();
  }

  void CallSeekAndBlock(bool is_seek_to, int64_t offset_or_position) {
    response_wait_loop_ = std::make_unique<base::RunLoop>();

    // We need to supply a serial or the test will crash.
    const std::string method_name = is_seek_to ? "SetPosition" : "Seek";
    dbus::MethodCall method_call(kMprisAPIPlayerInterfaceName, method_name);
    method_call.SetSerial(kFakeSerial);

    dbus::MessageWriter writer(&method_call);

    if (is_seek_to)
      writer.AppendObjectPath(
          dbus::ObjectPath("/org/chromium/MediaPlayer2/TrackList/TrackFooId"));

    writer.AppendInt64(offset_or_position);

    // Call the method and await a response.
    player_interface_exported_methods_[method_name].Run(
        &method_call,
        base::BindRepeating(&SystemMediaControlsLinuxTest::OnResponse,
                            base::Unretained(this)));
    response_wait_loop_->Run();
  }

  int64_t GetCurrentPositionValue() {
    base::RunLoop wait_loop;

    // We need to supply a serial or the test will crash.
    dbus::MethodCall method_call(DBUS_INTERFACE_PROPERTIES, "Get");
    method_call.SetSerial(kFakeSerial);
    dbus::MessageWriter writer(&method_call);
    writer.AppendString(kMprisAPIPlayerInterfaceName);
    writer.AppendString("Position");

    int64_t position;

    // Call the method and await a response.
    properties_interface_exported_methods_["Get"].Run(
        &method_call, base::BindOnce(
                          [](int64_t* position_out, base::RunLoop& wait_loop,
                             std::unique_ptr<dbus::Response> response) {
                            // A response of nullptr means an error has
                            // occurred.
                            EXPECT_NE(nullptr, response.get());

                            dbus::MessageReader reader(response.get());
                            ASSERT_TRUE(reader.PopVariantOfInt64(position_out));

                            wait_loop.Quit();
                          },
                          &position, std::ref(wait_loop)));
    wait_loop.Run();

    return position;
  }

  SystemMediaControlsLinux* GetService() { return service_.get(); }

  dbus::MockExportedObject* GetExportedObject() {
    return mock_exported_object_.get();
  }

  void AdvanceClockMilliseconds(int ms) {
    task_environment_.FastForwardBy(base::Milliseconds(ms));
  }

 private:
  void StartMprisServiceAndWaitForReady() {
    service_wait_loop_ = std::make_unique<base::RunLoop>();
    service_ = std::make_unique<SystemMediaControlsLinux>(kTestProductName);

    SetUpMocks();

    service_->SetBusForTesting(mock_bus_);
    service_->AddObserver(this);
    service_->StartService();
    service_wait_loop_->Run();
  }

  // Sets up the mock Bus and ExportedObject. The ExportedObject will store the
  // org.mpris.MediaPlayer2.Player exported methods in the
  // |player_interface_exported_methods_| map so we can call them for testing.
  void SetUpMocks() {
    dbus::Bus::Options options;
    options.bus_type = dbus::Bus::SESSION;
    options.connection_type = dbus::Bus::PRIVATE;
    mock_bus_ = base::MakeRefCounted<dbus::MockBus>(std::move(options));
    mock_exported_object_ = base::MakeRefCounted<dbus::MockExportedObject>(
        mock_bus_.get(), dbus::ObjectPath(kMprisAPIObjectPath));

    EXPECT_CALL(*mock_bus_,
                GetExportedObject(dbus::ObjectPath(kMprisAPIObjectPath)))
        .WillOnce(Return(mock_exported_object_.get()));
    EXPECT_CALL(*mock_bus_, RequestOwnership(service_->GetServiceName(), _, _))
        .WillOnce(Invoke(this, &SystemMediaControlsLinuxTest::OnOwnership));

    EXPECT_CALL(*mock_exported_object_, ExportMethod(_, _, _, _))
        .WillRepeatedly(
            Invoke(this, &SystemMediaControlsLinuxTest::OnExported));
  }

  // Tell the service that ownership was successful.
  void OnOwnership(const std::string& service_name,
                   Unused,
                   dbus::Bus::OnOwnershipCallback callback) {
    std::move(callback).Run(service_name, true);
  }

  // Store the exported method if necessary and tell the service that the export
  // was successful.
  void OnExported(const std::string& interface_name,
                  const std::string& method_name,
                  dbus::ExportedObject::MethodCallCallback exported_method,
                  dbus::ExportedObject::OnExportedCallback callback) {
    if (interface_name == kMprisAPIPlayerInterfaceName)
      player_interface_exported_methods_[method_name] = exported_method;
    if (interface_name == DBUS_INTERFACE_PROPERTIES)
      properties_interface_exported_methods_[method_name] = exported_method;
    std::move(callback).Run(interface_name, method_name, true);
  }

  void OnResponse(std::unique_ptr<dbus::Response> response) {
    // A response of nullptr means an error has occurred.
    EXPECT_NE(nullptr, response.get());
    if (response_wait_loop_)
      response_wait_loop_->Quit();
  }

  // SystemMediaControlsObserver implementation.
  void OnServiceReady() override {
    if (service_wait_loop_)
      service_wait_loop_->Quit();
  }

  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<base::RunLoop> service_wait_loop_;
  std::unique_ptr<base::RunLoop> response_wait_loop_;
  scoped_refptr<dbus::MockBus> mock_bus_;
  scoped_refptr<dbus::MockExportedObject> mock_exported_object_;

  base::flat_map<std::string, dbus::ExportedObject::MethodCallCallback>
      player_interface_exported_methods_;
  base::flat_map<std::string, dbus::ExportedObject::MethodCallCallback>
      properties_interface_exported_methods_;

  // `service_` field is last, because it contains `raw_ptr` to
  // `dbus::ExportedObject` in the maps above.  Destroying the `service_` field
  // first means that the `raw_ptr` doesn't become temporarily dangling.
  std::unique_ptr<SystemMediaControlsLinux> service_;
};

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfServiceReadyWhenAdded) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnServiceReady());
  AddObserver(&observer);
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfNextCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnNext(GetService()));
  AddObserver(&observer);
  CallMediaPlayer2PlayerMethodAndBlock("Next");
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfPreviousCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnPrevious(GetService()));
  AddObserver(&observer);
  CallMediaPlayer2PlayerMethodAndBlock("Previous");
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfPauseCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnPause(GetService()));
  AddObserver(&observer);
  CallMediaPlayer2PlayerMethodAndBlock("Pause");
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfPlayPauseCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnPlayPause(GetService()));
  AddObserver(&observer);
  CallMediaPlayer2PlayerMethodAndBlock("PlayPause");
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfStopCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnStop(GetService()));
  AddObserver(&observer);
  CallMediaPlayer2PlayerMethodAndBlock("Stop");
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfPlayCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnPlay(GetService()));
  AddObserver(&observer);
  CallMediaPlayer2PlayerMethodAndBlock("Play");
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfSeekCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnSeek(GetService(), base::Seconds(3)));
  AddObserver(&observer);
  CallSeekAndBlock(/*is_seek_to=*/false, base::Seconds(3).InMicroseconds());
}

TEST_F(SystemMediaControlsLinuxTest, ObserverNotifiedOfSetPositionCalls) {
  MockSystemMediaControlsObserver observer;
  EXPECT_CALL(observer, OnSeekTo(GetService(), base::Seconds(7)));
  AddObserver(&observer);
  CallSeekAndBlock(/*is_seek_to=*/true, base::Seconds(7).InMicroseconds());
}

TEST_F(SystemMediaControlsLinuxTest, ChangingPropertyEmitsSignal) {
  base::RunLoop wait_for_signal;

  // The returned signal should give the changed property.
  EXPECT_CALL(*GetExportedObject(), SendSignal(_))
      .WillOnce(WithArg<0>([&wait_for_signal](dbus::Signal* signal) {
        EXPECT_NE(nullptr, signal);
        dbus::MessageReader reader(signal);

        std::string interface_name;
        ASSERT_TRUE(reader.PopString(&interface_name));
        EXPECT_EQ(kMprisAPIPlayerInterfaceName, interface_name);

        dbus::MessageReader changed_properties_reader(nullptr);
        ASSERT_TRUE(reader.PopArray(&changed_properties_reader));

        dbus::MessageReader dict_entry_reader(nullptr);
        ASSERT_TRUE(changed_properties_reader.PopDictEntry(&dict_entry_reader));

        // The changed property name should be "CanPlay".
        std::string property_name;
        ASSERT_TRUE(dict_entry_reader.PopString(&property_name));
        EXPECT_EQ("CanGoNext", property_name);

        // The new value should be true.
        bool value;
        ASSERT_TRUE(dict_entry_reader.PopVariantOfBool(&value));
        EXPECT_EQ(true, value);

        // CanPlay should be the only entry.
        EXPECT_FALSE(changed_properties_reader.HasMoreData());

        wait_for_signal.Quit();
      }));

  // CanPlay is initialized as false, so setting it to true should emit an
  // org.freedesktop.DBus.Properties.PropertiesChanged signal.
  GetService()->SetIsNextEnabled(true);
  wait_for_signal.Run();

  // Setting it to true again should not re-signal.
  GetService()->SetIsNextEnabled(true);
}

TEST_F(SystemMediaControlsLinuxTest, ChangingMetadataEmitsSignal) {
  base::RunLoop wait_for_signal;

  // The returned signal should give the changed property.
  EXPECT_CALL(*GetExportedObject(), SendSignal(_))
      .WillOnce(WithArg<0>([&wait_for_signal](dbus::Signal* signal) {
        ASSERT_NE(nullptr, signal);
        dbus::MessageReader reader(signal);

        std::string interface_name;
        ASSERT_TRUE(reader.PopString(&interface_name));
        EXPECT_EQ(kMprisAPIPlayerInterfaceName, interface_name);

        dbus::MessageReader changed_properties_reader(nullptr);
        ASSERT_TRUE(reader.PopArray(&changed_properties_reader));

        dbus::MessageReader dict_entry_reader(nullptr);
        ASSERT_TRUE(changed_properties_reader.PopDictEntry(&dict_entry_reader));

        // The changed property name should be "Metadata".
        std::string property_name;
        ASSERT_TRUE(dict_entry_reader.PopString(&property_name));
        EXPECT_EQ("Metadata", property_name);

        // The new metadata should have the new title.
        dbus::MessageReader metadata_variant_reader(nullptr);
        ASSERT_TRUE(dict_entry_reader.PopVariant(&metadata_variant_reader));
        dbus::MessageReader metadata_reader(nullptr);
        ASSERT_TRUE(metadata_variant_reader.PopArray(&metadata_reader));

        dbus::MessageReader metadata_entry_reader(nullptr);
        ASSERT_TRUE(metadata_reader.PopDictEntry(&metadata_entry_reader));

        std::string metadata_property_name;
        ASSERT_TRUE(metadata_entry_reader.PopString(&metadata_property_name));
        EXPECT_EQ("xesam:title", metadata_property_name);

        std::string value;
        ASSERT_TRUE(metadata_entry_reader.PopVariantOfString(&value));
        EXPECT_EQ("Foo", value);

        // Metadata should be the only changed property.
        EXPECT_FALSE(changed_properties_reader.HasMoreData());

        wait_for_signal.Quit();
      }));

  // Setting the title should emit an
  // org.freedesktop.DBus.Properties.PropertiesChanged signal.
  GetService()->SetTitle(u"Foo");
  wait_for_signal.Run();

  // Setting the title to the same value as before should not emit a new signal.
  GetService()->SetTitle(u"Foo");
}

TEST_F(SystemMediaControlsLinuxTest, InvalidUTF8IsSanitized) {
  base::RunLoop wait_for_signal;

  // The returned signal should give the changed property.
  EXPECT_CALL(*GetExportedObject(), SendSignal(_))
      .WillOnce(WithArg<0>([&wait_for_signal](dbus::Signal* signal) {
        ASSERT_NE(nullptr, signal);
        dbus::MessageReader reader(signal);

        std::string interface_name;
        ASSERT_TRUE(reader.PopString(&interface_name));
        EXPECT_EQ(kMprisAPIPlayerInterfaceName, interface_name);

        dbus::MessageReader changed_properties_reader(nullptr);
        ASSERT_TRUE(reader.PopArray(&changed_properties_reader));

        dbus::MessageReader dict_entry_reader(nullptr);
        ASSERT_TRUE(changed_properties_reader.PopDictEntry(&dict_entry_reader));

        // The changed property name should be "Metadata".
        std::string property_name;
        ASSERT_TRUE(dict_entry_reader.PopString(&property_name));
        EXPECT_EQ("Metadata", property_name);

        // The new metadata should have the title with the noncharacter removed.
        dbus::MessageReader metadata_variant_reader(nullptr);
        ASSERT_TRUE(dict_entry_reader.PopVariant(&metadata_variant_reader));
        dbus::MessageReader metadata_reader(nullptr);
        ASSERT_TRUE(metadata_variant_reader.PopArray(&metadata_reader));

        dbus::MessageReader metadata_entry_reader(nullptr);
        ASSERT_TRUE(metadata_reader.PopDictEntry(&metadata_entry_reader));

        std::string metadata_property_name;
        ASSERT_TRUE(metadata_entry_reader.PopString(&metadata_property_name));
        EXPECT_EQ("xesam:title", metadata_property_name);

        std::string value;
        ASSERT_TRUE(metadata_entry_reader.PopVariantOfString(&value));
        EXPECT_EQ("TitleContainingNoncharacter", value);

        // Metadata should be the only changed property.
        EXPECT_FALSE(changed_properties_reader.HasMoreData());

        wait_for_signal.Quit();
      }));

  GetService()->SetTitle(u"Title\uFFFFContainingNoncharacter");
  wait_for_signal.Run();
}

TEST_F(SystemMediaControlsLinuxTest,
       PlayingMediaWithPositionWillContinuouslyUpdatePosition) {
  base::RunLoop wait_for_initial_position_update;

  const base::TimeDelta expected_position = base::Seconds(5);
  double expected_rate = 2.0;
  const base::TimeDelta expected_duration = base::Seconds(20);

  // Since the position is updated every 500ms, and the rate is 2.0, after 500ms
  // we should get 6 seconds as the position.
  const base::TimeDelta expected_updated_position = base::Seconds(6);

  int signal_count = 0;

  // The returned signal should give the changed property.
  EXPECT_CALL(*GetExportedObject(), SendSignal(_))
      .WillRepeatedly(WithArg<0>([&](dbus::Signal* signal) {
        signal_count++;

        EXPECT_NE(nullptr, signal);
        dbus::MessageReader reader(signal);

        // The final signal we get is a "Seeked" signal which has a different
        // format than the rest.
        if (signal_count == 4) {
          EXPECT_EQ(kMprisAPIPlayerInterfaceName, signal->GetInterface());
          EXPECT_EQ(kMprisAPISignalSeeked, signal->GetMember());

          int64_t new_position;
          ASSERT_TRUE(reader.PopInt64(&new_position));
          EXPECT_EQ(expected_position.InMicroseconds(), new_position);

          wait_for_initial_position_update.Quit();
          return;
        }

        EXPECT_EQ(DBUS_INTERFACE_PROPERTIES, signal->GetInterface());
        EXPECT_EQ("PropertiesChanged", signal->GetMember());

        std::string interface_name;
        ASSERT_TRUE(reader.PopString(&interface_name));
        EXPECT_EQ(kMprisAPIPlayerInterfaceName, interface_name);

        dbus::MessageReader changed_properties_reader(nullptr);
        ASSERT_TRUE(reader.PopArray(&changed_properties_reader));

        dbus::MessageReader dict_entry_reader(nullptr);
        ASSERT_TRUE(changed_properties_reader.PopDictEntry(&dict_entry_reader));

        std::string property_name;
        std::string metadata_property_name;
        dbus::MessageReader metadata_variant_reader(nullptr);
        dbus::MessageReader metadata_reader(nullptr);
        dbus::MessageReader metadata_entry_reader(nullptr);
        std::string playback_status_value;
        double rate_value;
        int64_t duration_value;

        ASSERT_TRUE(dict_entry_reader.PopString(&property_name));
        switch (signal_count) {
          case 1:
            // The first changed property will be the playback status to
            // playing.
            EXPECT_EQ("PlaybackStatus", property_name);
            ASSERT_TRUE(
                dict_entry_reader.PopVariantOfString(&playback_status_value));
            EXPECT_EQ("Playing", playback_status_value);
            break;
          case 2:
            // The next changed property will be rate to 1.0.
            EXPECT_EQ("Rate", property_name);
            ASSERT_TRUE(dict_entry_reader.PopVariantOfDouble(&rate_value));
            EXPECT_EQ(expected_rate, rate_value);
            break;
          case 3:
            // The next changed property will be duration to 20 seconds.
            EXPECT_EQ("Metadata", property_name);
            ASSERT_TRUE(dict_entry_reader.PopVariant(&metadata_variant_reader));
            ASSERT_TRUE(metadata_variant_reader.PopArray(&metadata_reader));
            ASSERT_TRUE(metadata_reader.PopDictEntry(&metadata_entry_reader));
            ASSERT_TRUE(
                metadata_entry_reader.PopString(&metadata_property_name));
            EXPECT_EQ("mpris:length", metadata_property_name);
            ASSERT_TRUE(
                metadata_entry_reader.PopVariantOfInt64(&duration_value));
            EXPECT_EQ(expected_duration.InMicroseconds(), duration_value);
            break;
        }

        // There should only be one entry at a time.
        EXPECT_FALSE(changed_properties_reader.HasMoreData());
      }));

  // Set playback status to "Playing" to ensure the position updates.
  GetService()->SetPlaybackStatus(
      SystemMediaControls::PlaybackStatus::kPlaying);

  // Set the initial position.
  media_session::MediaPosition position(expected_rate, expected_duration,
                                        expected_position,
                                        /*end_of_media=*/false);
  GetService()->SetPosition(position);

  // Wait for the initial position property updates to be signaled.
  wait_for_initial_position_update.Run();

  // After the initial position signaling, we should not receive more signals
  // for the position updates that happen due to typical media playback.
  testing::Mock::VerifyAndClearExpectations(GetExportedObject());
  EXPECT_CALL(*GetExportedObject(), SendSignal(_)).Times(0);

  // Even without signals, the property should still be updated and return the
  // correct new value when called after some time.
  AdvanceClockMilliseconds(500);
  EXPECT_EQ(expected_updated_position.InMicroseconds(),
            GetCurrentPositionValue());

  const base::TimeDelta expected_seeked_position = base::Seconds(14);
  base::RunLoop wait_for_seeked_signal;

  // If the position changes in a way that is inconsistent with the current
  // playing state (e.g. the user has seeked to a different time), then we
  // should receive a "Seeked" signal indicating the change.
  testing::Mock::VerifyAndClearExpectations(GetExportedObject());
  EXPECT_CALL(*GetExportedObject(), SendSignal(_))
      .WillRepeatedly(WithArg<0>([&](dbus::Signal* signal) {
        EXPECT_NE(nullptr, signal);
        EXPECT_EQ(kMprisAPIPlayerInterfaceName, signal->GetInterface());
        EXPECT_EQ(kMprisAPISignalSeeked, signal->GetMember());

        dbus::MessageReader reader(signal);
        int64_t new_position;
        ASSERT_TRUE(reader.PopInt64(&new_position));
        EXPECT_EQ(expected_seeked_position.InMicroseconds(), new_position);

        wait_for_seeked_signal.Quit();
      }));

  media_session::MediaPosition seeked_position(expected_rate, expected_duration,
                                               expected_seeked_position,
                                               /*end_of_media=*/false);
  GetService()->SetPosition(seeked_position);
  wait_for_seeked_signal.Run();
}

TEST_F(SystemMediaControlsLinuxTest, ChangingIdEmitsSignal) {
  base::RunLoop wait_for_signal;

  // The returned signal should give the new Id.
  EXPECT_CALL(*GetExportedObject(), SendSignal(_))
      .WillOnce(WithArg<0>([&wait_for_signal](dbus::Signal* signal) {
        ASSERT_NE(nullptr, signal);
        dbus::MessageReader reader(signal);

        std::string interface_name;
        ASSERT_TRUE(reader.PopString(&interface_name));
        EXPECT_EQ(kMprisAPIPlayerInterfaceName, interface_name);

        dbus::MessageReader changed_properties_reader(nullptr);
        ASSERT_TRUE(reader.PopArray(&changed_properties_reader));

        dbus::MessageReader dict_entry_reader(nullptr);
        ASSERT_TRUE(changed_properties_reader.PopDictEntry(&dict_entry_reader));

        // The changed property name should be "Metadata".
        std::string property_name;
        ASSERT_TRUE(dict_entry_reader.PopString(&property_name));
        EXPECT_EQ("Metadata", property_name);

        // The new metadata should have the new Id.
        dbus::MessageReader metadata_variant_reader(nullptr);
        ASSERT_TRUE(dict_entry_reader.PopVariant(&metadata_variant_reader));
        dbus::MessageReader metadata_reader(nullptr);
        ASSERT_TRUE(metadata_variant_reader.PopArray(&metadata_reader));

        dbus::MessageReader metadata_entry_reader(nullptr);
        ASSERT_TRUE(metadata_reader.PopDictEntry(&metadata_entry_reader));

        std::string metadata_property_name;
        ASSERT_TRUE(metadata_entry_reader.PopString(&metadata_property_name));
        EXPECT_EQ("mpris:trackid", metadata_property_name);

        dbus::ObjectPath value;
        ASSERT_TRUE(metadata_entry_reader.PopVariantOfObjectPath(&value));
        EXPECT_EQ("/org/chromium/MediaPlayer2/TrackList/TrackFooId",
                  value.value());

        // Metadata should be the only changed property.
        EXPECT_FALSE(changed_properties_reader.HasMoreData());

        wait_for_signal.Quit();
      }));

  // Setting the ID should emit an
  // org.freedesktop.DBus.Properties.PropertiesChanged signal.
  const std::string given_id("FooId");
  GetService()->SetID(&given_id);
  wait_for_signal.Run();
}

}  // namespace internal

}  // namespace system_media_controls
