// Copyright 2024 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/audio_ducker.h"

#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/chrome_test_path_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/media_session_service.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/media_session/public/cpp/test/audio_focus_test_util.h"

namespace {

constexpr base::FilePath::CharType kTestPage[] =
    FILE_PATH_LITERAL("media/bigbuck-player.html");

constexpr base::FilePath::CharType kTestPageStartWithNoPlayer[] =
    FILE_PATH_LITERAL("media/start-with-no-player.html");

}  // namespace

using media_session::mojom::MediaSessionInfo;

class AudioDuckerBrowserTest : public InProcessBrowserTest {
 public:
  AudioDuckerBrowserTest() = default;
  AudioDuckerBrowserTest(const AudioDuckerBrowserTest&) = delete;
  AudioDuckerBrowserTest& operator=(const AudioDuckerBrowserTest&) = delete;
  ~AudioDuckerBrowserTest() override = default;

  void SetUpOnMainThread() override {
    ResetAudioFocusObserver();

    host_resolver()->AddRule("*", "127.0.0.1");
    embedded_test_server()->ServeFilesFromSourceDirectory("chrome/test/data");
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  void AddVideo(content::WebContents& web_contents) {
    web_contents.GetPrimaryMainFrame()
        ->ExecuteJavaScriptWithUserGestureForTests(
            u"addVideo();", base::NullCallback(),
            content::ISOLATED_WORLD_ID_GLOBAL);
  }

  void PlayVideoAndWaitForAudioFocus(content::WebContents& web_contents) {
    ResetAudioFocusObserver();
    web_contents.GetPrimaryMainFrame()
        ->ExecuteJavaScriptWithUserGestureForTests(
            u"document.getElementsByTagName('video')[0].play()",
            base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL);
    WaitForAudioFocusGained();
  }

  void ResetAudioFocusObserver() {
    mojo::Remote<media_session::mojom::AudioFocusManager> audio_focus_remote;
    content::GetMediaSessionService().BindAudioFocusManager(
        audio_focus_remote.BindNewPipeAndPassReceiver());
    audio_focus_observer_ =
        std::make_unique<media_session::test::TestAudioFocusObserver>();
    audio_focus_remote->AddObserver(
        audio_focus_observer_->BindNewPipeAndPassRemote());
  }

  void WaitForAudioFocusGained() {
    audio_focus_observer_->WaitForGainedEvent();
  }

  void FlushAudioFocusManager() {
    mojo::Remote<media_session::mojom::AudioFocusManager> audio_focus_remote;
    content::GetMediaSessionService().BindAudioFocusManager(
        audio_focus_remote.BindNewPipeAndPassReceiver());

    base::RunLoop flush_waiter;
    audio_focus_remote->FlushForTesting(flush_waiter.QuitClosure());
    flush_waiter.Run();
  }

  void ExpectMediaSessionState(content::MediaSession& media_session,
                               MediaSessionInfo::SessionState expected_state) {
    base::RunLoop waiter;
    media_session.GetMediaSessionInfo(base::BindOnce(
        [](MediaSessionInfo::SessionState expected_state,
           base::OnceClosure wait_closure,
           media_session::mojom::MediaSessionInfoPtr info) {
          EXPECT_EQ(info->state, expected_state);
          std::move(wait_closure).Run();
        },
        expected_state, waiter.QuitClosure()));
    waiter.Run();
  }

 private:
  std::unique_ptr<media_session::test::TestAudioFocusObserver>
      audio_focus_observer_;
};

IN_PROC_BROWSER_TEST_F(AudioDuckerBrowserTest,
                       DucksAudioInOtherTabs_MediaPlaying) {
  GURL test_page_url = chrome_test_utils::GetTestUrl(
      base::FilePath(base::FilePath::kCurrentDirectory),
      base::FilePath(kTestPage));

  // Open a test page and start playing a video.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page_url));
  content::WebContents* web_contents1 =
      browser()->tab_strip_model()->GetActiveWebContents();
  PlayVideoAndWaitForAudioFocus(*web_contents1);

  // Open a second test page and also play a video.
  ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
      browser(), test_page_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
  content::WebContents* web_contents2 =
      browser()->tab_strip_model()->GetActiveWebContents();
  PlayVideoAndWaitForAudioFocus(*web_contents2);

  content::MediaSession* media_session1 =
      content::MediaSession::GetIfExists(web_contents1);
  content::MediaSession* media_session2 =
      content::MediaSession::GetIfExists(web_contents2);
  ASSERT_TRUE(media_session1);
  ASSERT_TRUE(media_session2);

  AudioDucker* audio_ducker =
      AudioDucker::GetOrCreateForPage(web_contents2->GetPrimaryPage());

  // Neither page should be ducked.
  ExpectMediaSessionState(*media_session1,
                          MediaSessionInfo::SessionState::kActive);
  ExpectMediaSessionState(*media_session2,
                          MediaSessionInfo::SessionState::kActive);
  EXPECT_EQ(AudioDucker::AudioDuckingState::kNoDucking,
            audio_ducker->GetAudioDuckingState());

  // Tell the AudioDucker associated with the second page to duck other audio.
  EXPECT_TRUE(audio_ducker->StartDuckingOtherAudio());
  FlushAudioFocusManager();

  // The first page should be ducked while the second remains unducked.
  ExpectMediaSessionState(*media_session1,
                          MediaSessionInfo::SessionState::kDucking);
  ExpectMediaSessionState(*media_session2,
                          MediaSessionInfo::SessionState::kActive);
  EXPECT_EQ(AudioDucker::AudioDuckingState::kDucking,
            audio_ducker->GetAudioDuckingState());

  // Tell the AudioDucker to stop ducking.
  EXPECT_TRUE(audio_ducker->StopDuckingOtherAudio());
  FlushAudioFocusManager();

  // Neither page should be ducked.
  ExpectMediaSessionState(*media_session1,
                          MediaSessionInfo::SessionState::kActive);
  ExpectMediaSessionState(*media_session2,
                          MediaSessionInfo::SessionState::kActive);
  EXPECT_EQ(AudioDucker::AudioDuckingState::kNoDucking,
            audio_ducker->GetAudioDuckingState());
}

IN_PROC_BROWSER_TEST_F(AudioDuckerBrowserTest,
                       DucksAudioInOtherTabs_NoMediaPlaying) {
  GURL test_page_url = chrome_test_utils::GetTestUrl(
      base::FilePath(base::FilePath::kCurrentDirectory),
      base::FilePath(kTestPage));

  // Open a test page and start playing a video.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page_url));
  content::WebContents* web_contents1 =
      browser()->tab_strip_model()->GetActiveWebContents();
  PlayVideoAndWaitForAudioFocus(*web_contents1);

  // Open a second test page that has no video.
  GURL test_page_no_player_url = chrome_test_utils::GetTestUrl(
      base::FilePath(base::FilePath::kCurrentDirectory),
      base::FilePath(kTestPageStartWithNoPlayer));

  ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
      browser(), test_page_no_player_url,
      WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
  content::WebContents* web_contents2 =
      browser()->tab_strip_model()->GetActiveWebContents();

  content::MediaSession* media_session1 =
      content::MediaSession::GetIfExists(web_contents1);
  ASSERT_TRUE(media_session1);
  ASSERT_EQ(nullptr, content::MediaSession::GetIfExists(web_contents2));

  AudioDucker* audio_ducker =
      AudioDucker::GetOrCreateForPage(web_contents2->GetPrimaryPage());

  // The first page should not be ducked.
  ExpectMediaSessionState(*media_session1,
                          MediaSessionInfo::SessionState::kActive);
  EXPECT_EQ(AudioDucker::AudioDuckingState::kNoDucking,
            audio_ducker->GetAudioDuckingState());

  // Tell the AudioDucker associated with the second page to duck other audio.
  EXPECT_TRUE(audio_ducker->StartDuckingOtherAudio());
  FlushAudioFocusManager();

  // The first page should be ducked.
  ExpectMediaSessionState(*media_session1,
                          MediaSessionInfo::SessionState::kDucking);
  EXPECT_EQ(AudioDucker::AudioDuckingState::kDucking,
            audio_ducker->GetAudioDuckingState());

  // Add a video into the second page and play it.
  AddVideo(*web_contents2);
  PlayVideoAndWaitForAudioFocus(*web_contents2);
  content::MediaSession* media_session2 =
      content::MediaSession::GetIfExists(web_contents2);
  ASSERT_TRUE(media_session2);

  // The first page should still be ducked but the second page should not be
  // ducked.
  ExpectMediaSessionState(*media_session1,
                          MediaSessionInfo::SessionState::kDucking);
  ExpectMediaSessionState(*media_session2,
                          MediaSessionInfo::SessionState::kActive);
  EXPECT_EQ(AudioDucker::AudioDuckingState::kDucking,
            audio_ducker->GetAudioDuckingState());

  // Tell the AudioDucker to stop ducking.
  EXPECT_TRUE(audio_ducker->StopDuckingOtherAudio());
  FlushAudioFocusManager();

  // The first page should no longer be ducked.
  ExpectMediaSessionState(*media_session1,
                          MediaSessionInfo::SessionState::kActive);
  ExpectMediaSessionState(*media_session2,
                          MediaSessionInfo::SessionState::kActive);
  EXPECT_EQ(AudioDucker::AudioDuckingState::kNoDucking,
            audio_ducker->GetAudioDuckingState());
}
