// 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 "content/public/browser/media_session.h"

#include <optional>

#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_features.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/media_start_stop_observer.h"
#include "content/public/test/test_media_session_client.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "media/base/media_switches.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "services/media_session/public/cpp/features.h"
#include "services/media_session/public/cpp/test/audio_focus_test_util.h"
#include "services/media_session/public/cpp/test/mock_media_session.h"

namespace {
int hidden_metadata_placeholder_thumbnail_size = 42;
}  // namespace

namespace content {

namespace {

const char kMediaSessionImageTestURL[] = "/media/session/image_test_page.html";
const char kMediaSessionImageTestPageVideoElement[] = "video";

const char kMediaSessionTestImagePath[] = "/media/session/test_image.jpg";

class MediaImageGetterHelper {
 public:
  MediaImageGetterHelper(content::MediaSession* media_session,
                         const media_session::MediaImage& image,
                         int min_size,
                         int desired_size) {
    media_session->GetMediaImageBitmap(
        image, min_size, desired_size,
        base::BindOnce(&MediaImageGetterHelper::OnComplete,
                       base::Unretained(this)));
  }

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

  void Wait() {
    if (bitmap_.has_value())
      return;

    run_loop_.Run();
  }

  const SkBitmap& bitmap() { return *bitmap_; }

 private:
  void OnComplete(const SkBitmap& bitmap) {
    bitmap_ = bitmap;
    run_loop_.Quit();
  }

  base::RunLoop run_loop_;
  std::optional<SkBitmap> bitmap_;
};

// Integration tests for content::MediaSession that do not take into
// consideration the implementation details contrary to
// MediaSessionImplBrowserTest.
class MediaSessionBrowserTestBase : public ContentBrowserTest {
 public:
  MediaSessionBrowserTestBase() {
    embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
        &MediaSessionBrowserTestBase::OnServerRequest, base::Unretained(this)));
  }

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

  void SetUp() override {
    ContentBrowserTest::SetUp();
    visited_urls_.clear();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitchASCII(
        switches::kAutoplayPolicy,
        switches::autoplay::kNoUserGestureRequiredPolicy);
  }

  void StartPlaybackAndWait(Shell* shell, const std::string& id) {
    shell->web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests(
        u"document.querySelector('#" + base::ASCIIToUTF16(id) + u"').play();",
        base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL);
    WaitForStart(shell);
  }

  void StopPlaybackAndWait(Shell* shell, const std::string& id) {
    shell->web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests(
        u"document.querySelector('#" + base::ASCIIToUTF16(id) + u"').pause();",
        base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL);
    WaitForStop(shell);
  }

  void WaitForStart(Shell* shell) {
    MediaStartStopObserver observer(shell->web_contents(),
                                    MediaStartStopObserver::Type::kStart);
    observer.Wait();
  }

  void WaitForStop(Shell* shell) {
    MediaStartStopObserver observer(shell->web_contents(),
                                    MediaStartStopObserver::Type::kStop);
    observer.Wait();
  }

  bool IsPlaying(Shell* shell, const std::string& id) {
    return EvalJs(shell->web_contents(),
                  "!document.querySelector('#" + id + "').paused;")
        .ExtractBool();
  }

  bool WasURLVisited(const GURL& url) {
    base::AutoLock lock(visited_urls_lock_);
    return visited_urls_.contains(url);
  }

  MediaSession* SetupMediaImageTest(bool expect_media_image = true) {
    EXPECT_TRUE(NavigateToURL(
        shell(), embedded_test_server()->GetURL(kMediaSessionImageTestURL)));
    StartPlaybackAndWait(shell(), kMediaSessionImageTestPageVideoElement);

    MediaSession* media_session = MediaSession::Get(shell()->web_contents());

    if (expect_media_image) {
      std::vector<media_session::MediaImage> expected_images;

      expected_images.push_back(CreateTestImageWithSize(1));
      expected_images.push_back(CreateTestImageWithSize(10));
      media_session::test::MockMediaSessionMojoObserver observer(
          *media_session);
      observer.WaitForExpectedImagesOfType(
          media_session::mojom::MediaSessionImageType::kArtwork,
          expected_images);
    }

    return media_session;
  }

  media_session::MediaImage CreateTestImageWithSize(int size) const {
    media_session::MediaImage image;
    image.src = GetTestImageURL();
    image.type = u"image/jpeg";
    image.sizes.push_back(gfx::Size(size, size));
    return image;
  }

  GURL GetTestImageURL() const {
    return embedded_test_server()->GetURL(kMediaSessionTestImagePath);
  }

 private:
  void OnServerRequest(const net::test_server::HttpRequest& request) {
    // Note this method is called on the EmbeddedTestServer's background thread.
    base::AutoLock lock(visited_urls_lock_);
    visited_urls_.insert(request.GetURL());
  }

  // visited_urls_ is accessed both on the main thread and on the
  // EmbeddedTestServer's background thread via OnServerRequest(), so it must be
  // locked.
  base::Lock visited_urls_lock_;
  std::set<GURL> visited_urls_;
};

class MediaSessionBrowserTest : public MediaSessionBrowserTestBase {
 public:
  MediaSessionBrowserTest() {
    feature_list_.InitAndEnableFeature(media::kInternalMediaSession);
  }

  void SetUp() override {
    SetupMediaSessionClient();

    MediaSessionBrowserTestBase::SetUp();
  }

 protected:
  void SetupMediaSessionClient() {
    SkBitmap placeholder_bitmap;
    placeholder_bitmap.allocN32Pixels(
        hidden_metadata_placeholder_thumbnail_size,
        hidden_metadata_placeholder_thumbnail_size);
    client_.SetThumbnailPlaceholder(placeholder_bitmap);
  }

  TestMediaSessionClient client_;

 private:
  base::test::ScopedFeatureList feature_list_;
};

class MediaSessionBrowserTestWithoutInternalMediaSession
    : public MediaSessionBrowserTestBase {
 public:
  MediaSessionBrowserTestWithoutInternalMediaSession() {
    disabled_feature_list_.InitWithFeatures(
        {}, {media::kInternalMediaSession,
             media_session::features::kMediaSessionService});
  }

 private:
  base::test::ScopedFeatureList disabled_feature_list_;
};

}  // anonymous namespace

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTestWithoutInternalMediaSession,
                       MediaSessionNoOpWhenDisabled) {
  EXPECT_TRUE(NavigateToURL(shell(),
                            GetTestUrl("media/session", "media-session.html")));

  MediaSession* media_session = MediaSession::Get(shell()->web_contents());
  ASSERT_NE(nullptr, media_session);

  StartPlaybackAndWait(shell(), "long-video");
  StartPlaybackAndWait(shell(), "long-audio");

  media_session->Suspend(MediaSession::SuspendType::kSystem);
  StopPlaybackAndWait(shell(), "long-audio");

  // At that point, only "long-audio" is paused.
  EXPECT_FALSE(IsPlaying(shell(), "long-audio"));
  EXPECT_TRUE(IsPlaying(shell(), "long-video"));
}

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, SimplePlayPause) {
  EXPECT_TRUE(NavigateToURL(shell(),
                            GetTestUrl("media/session", "media-session.html")));

  MediaSession* media_session = MediaSession::Get(shell()->web_contents());
  ASSERT_NE(nullptr, media_session);

  StartPlaybackAndWait(shell(), "long-video");

  media_session->Suspend(MediaSession::SuspendType::kSystem);
  WaitForStop(shell());
  EXPECT_FALSE(IsPlaying(shell(), "long-video"));

  media_session->Resume(MediaSession::SuspendType::kSystem);
  WaitForStart(shell());
  EXPECT_TRUE(IsPlaying(shell(), "long-video"));
}

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, MultiplePlayersPlayPause) {
  EXPECT_TRUE(NavigateToURL(shell(),
                            GetTestUrl("media/session", "media-session.html")));

  MediaSession* media_session = MediaSession::Get(shell()->web_contents());
  ASSERT_NE(nullptr, media_session);

  StartPlaybackAndWait(shell(), "long-video");
  StartPlaybackAndWait(shell(), "long-audio");

  media_session->Suspend(MediaSession::SuspendType::kSystem);
  WaitForStop(shell());
  EXPECT_FALSE(IsPlaying(shell(), "long-video"));
  EXPECT_FALSE(IsPlaying(shell(), "long-audio"));

  media_session->Resume(MediaSession::SuspendType::kSystem);
  WaitForStart(shell());
  EXPECT_TRUE(IsPlaying(shell(), "long-video"));
  EXPECT_TRUE(IsPlaying(shell(), "long-audio"));
}

// Flaky on Mac. See https://crbug.com/980663
#if BUILDFLAG(IS_MAC)
#define MAYBE_WebContents_Muted DISABLED_WebContents_Muted
#else
#define MAYBE_WebContents_Muted WebContents_Muted
#endif
IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, MAYBE_WebContents_Muted) {
  EXPECT_TRUE(NavigateToURL(shell(),
                            GetTestUrl("media/session", "media-session.html")));

  shell()->web_contents()->SetAudioMuted(true);
  MediaSession* media_session = MediaSession::Get(shell()->web_contents());
  ASSERT_NE(nullptr, media_session);

  StartPlaybackAndWait(shell(), "long-video");
  EXPECT_FALSE(media_session::test::GetMediaSessionInfoSync(media_session)
                   ->is_controllable);

  // Unmute the web contents and the player should be created.
  shell()->web_contents()->SetAudioMuted(false);
  EXPECT_TRUE(media_session::test::GetMediaSessionInfoSync(media_session)
                  ->is_controllable);

  // Now mute it again and the player should be removed.
  shell()->web_contents()->SetAudioMuted(true);
  EXPECT_FALSE(media_session::test::GetMediaSessionInfoSync(media_session)
                   ->is_controllable);
}

#if !BUILDFLAG(IS_ANDROID)
// On Android, System Audio Focus would break this test.

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, MultipleTabsPlayPause) {
  Shell* other_shell = CreateBrowser();

  EXPECT_TRUE(NavigateToURL(shell(),
                            GetTestUrl("media/session", "media-session.html")));
  EXPECT_TRUE(NavigateToURL(other_shell,
                            GetTestUrl("media/session", "media-session.html")));

  MediaSession* media_session = MediaSession::Get(shell()->web_contents());
  MediaSession* other_media_session =
      MediaSession::Get(other_shell->web_contents());
  ASSERT_NE(nullptr, media_session);
  ASSERT_NE(nullptr, other_media_session);

  StartPlaybackAndWait(shell(), "long-video");
  StartPlaybackAndWait(other_shell, "long-video");

  media_session->Suspend(MediaSession::SuspendType::kSystem);
  WaitForStop(shell());
  EXPECT_FALSE(IsPlaying(shell(), "long-video"));
  EXPECT_TRUE(IsPlaying(other_shell, "long-video"));

  other_media_session->Suspend(MediaSession::SuspendType::kSystem);
  WaitForStop(other_shell);
  EXPECT_FALSE(IsPlaying(shell(), "long-video"));
  EXPECT_FALSE(IsPlaying(other_shell, "long-video"));

  media_session->Resume(MediaSession::SuspendType::kSystem);
  WaitForStart(shell());
  EXPECT_TRUE(IsPlaying(shell(), "long-video"));
  EXPECT_FALSE(IsPlaying(other_shell, "long-video"));

  other_media_session->Resume(MediaSession::SuspendType::kSystem);
  WaitForStart(other_shell);
  EXPECT_TRUE(IsPlaying(shell(), "long-video"));
  EXPECT_TRUE(IsPlaying(other_shell, "long-video"));
}
#endif  // BUILDFLAG(IS_ANDROID)

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, GetMediaImageBitmap) {
  ASSERT_TRUE(embedded_test_server()->Start());

  MediaSession* media_session = SetupMediaImageTest();
  ASSERT_NE(nullptr, media_session);

  MediaImageGetterHelper helper(media_session, CreateTestImageWithSize(1), 0,
                                10);
  helper.Wait();

  // The test image is a 1x1 test image.
  EXPECT_EQ(1, helper.bitmap().width());
  EXPECT_EQ(1, helper.bitmap().height());
  EXPECT_EQ(kRGBA_8888_SkColorType, helper.bitmap().colorType());

  EXPECT_TRUE(WasURLVisited(GetTestImageURL()));
}

// We hide the media image from CrOS' media controls by replacing the image in
// the MediaSessionImpl with a placeholder image. These changes are gated to
// only affect ChromeOS, hence why the testing for this is also ChromeOS only.
#if BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, HideMediaMetadataImageInCrOS) {
  client_.SetShouldHideMetadata(true);

  ASSERT_TRUE(embedded_test_server()->Start());

  // We don't expect a media image because of the way the image will be replaced
  // with its placeholder in MediaSessionImpl.
  MediaSession* media_session =
      SetupMediaImageTest(/*expect_media_image=*/false);
  ASSERT_NE(nullptr, media_session);

  MediaImageGetterHelper helper(media_session, CreateTestImageWithSize(1), 0,
                                10);

  helper.Wait();

  EXPECT_EQ(hidden_metadata_placeholder_thumbnail_size,
            helper.bitmap().width());
  EXPECT_EQ(hidden_metadata_placeholder_thumbnail_size,
            helper.bitmap().height());

  // As we are replacing the image, we should not visit the original's URL.
  EXPECT_FALSE(WasURLVisited(GetTestImageURL()));
}
#else  // !BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
                       DontHideMediaMetadataImageInNonCrOS) {
  client_.SetShouldHideMetadata(true);

  ASSERT_TRUE(embedded_test_server()->Start());

  MediaSession* media_session = SetupMediaImageTest();
  ASSERT_NE(nullptr, media_session);

  MediaImageGetterHelper helper(media_session, CreateTestImageWithSize(1), 0,
                                10);
  helper.Wait();

  // The test image is a 1x1 test image.
  EXPECT_EQ(1, helper.bitmap().width());
  EXPECT_EQ(1, helper.bitmap().height());
  EXPECT_EQ(kRGBA_8888_SkColorType, helper.bitmap().colorType());

  EXPECT_TRUE(WasURLVisited(GetTestImageURL()));
}
#endif

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
                       GetMediaImageBitmap_ImageTooSmall) {
  ASSERT_TRUE(embedded_test_server()->Start());

  MediaSession* media_session = SetupMediaImageTest();
  ASSERT_NE(nullptr, media_session);

  MediaImageGetterHelper helper(media_session, CreateTestImageWithSize(10), 10,
                                10);
  helper.Wait();

  // The |image| is too small but we do not know that until after we have
  // downloaded it. We should still receive a null image though.
  EXPECT_TRUE(helper.bitmap().isNull());
  EXPECT_TRUE(WasURLVisited(GetTestImageURL()));
}

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
                       GetMediaImageBitmap_ImageTooSmall_BeforeDownload) {
  ASSERT_TRUE(embedded_test_server()->Start());

  MediaSession* media_session = SetupMediaImageTest();
  ASSERT_NE(nullptr, media_session);

  MediaImageGetterHelper helper(media_session, CreateTestImageWithSize(1), 10,
                                10);
  helper.Wait();

  // Since |image| is too small but we know this in advance we should not
  // download it and instead we should receive a null image.
  EXPECT_TRUE(helper.bitmap().isNull());
  EXPECT_FALSE(WasURLVisited(GetTestImageURL()));
}

IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
                       GetMediaImageBitmap_InvalidImage) {
  ASSERT_TRUE(embedded_test_server()->Start());

  MediaSession* media_session = SetupMediaImageTest();
  ASSERT_NE(nullptr, media_session);

  media_session::MediaImage image = CreateTestImageWithSize(1);
  image.src = embedded_test_server()->GetURL("/blank.jpg");

  MediaImageGetterHelper helper(media_session, image, 0, 10);
  helper.Wait();

  // Since |image| is not an image that is associated with the test page we
  // should not download it and instead we should receive a null image.
  EXPECT_TRUE(helper.bitmap().isNull());
  EXPECT_FALSE(WasURLVisited(image.src));
}

// Regression test of crbug.com/1195769.
IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, ChangeMediaElementDocument) {
  ASSERT_TRUE(NavigateToURL(
      shell(), GetTestUrl("media/session", "change_document.html")));
  ASSERT_TRUE(ExecJs(shell()->web_contents(), "moveAudioToSubframe();"));

  ASSERT_EQ(true, EvalJs(shell(), "play();"));
  MediaSession* const media_session =
      MediaSession::Get(shell()->web_contents());
  media_session->Suspend(MediaSession::SuspendType::kUI);
  WaitForStop(shell());
}

}  // namespace content
