// 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 <algorithm>

#include "base/barrier_closure.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/api/desktop_capture/desktop_capture_api.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/sessions/tab_restore_service_load_waiter.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tab_sharing/tab_sharing_infobar_delegate.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "components/infobars/core/infobar_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "media/base/media_switches.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/gl/gl_switches.h"

namespace {
static const char kMainWebrtcTestHtmlPage[] = "/webrtc/webrtc_jsep01_test.html";

content::WebContents* GetWebContents(BrowserWindowInterface* browser, int tab) {
  return browser->GetTabStripModel()->GetWebContentsAt(tab);
}

content::DesktopMediaID GetDesktopMediaIDForScreen() {
  return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN,
                                 content::DesktopMediaID::kFakeId);
}

content::DesktopMediaID GetDesktopMediaIDForTab(BrowserWindowInterface* browser,
                                                int tab) {
  content::RenderFrameHost* main_frame =
      GetWebContents(browser, tab)->GetPrimaryMainFrame();
  return content::DesktopMediaID(
      content::DesktopMediaID::TYPE_WEB_CONTENTS,
      content::DesktopMediaID::kNullId,
      content::WebContentsMediaCaptureId(
          main_frame->GetProcess()->GetDeprecatedID(),
          main_frame->GetRoutingID()));
}

infobars::ContentInfoBarManager* GetInfoBarManager(
    BrowserWindowInterface* browser,
    int tab) {
  return infobars::ContentInfoBarManager::FromWebContents(
      GetWebContents(browser, tab));
}

infobars::ContentInfoBarManager* GetInfoBarManager(
    content::WebContents* contents) {
  return infobars::ContentInfoBarManager::FromWebContents(contents);
}

TabSharingInfoBarDelegate* GetDelegate(BrowserWindowInterface* browser,
                                       int tab) {
  return static_cast<TabSharingInfoBarDelegate*>(
      GetInfoBarManager(browser, tab)->infobars()[0]->delegate());
}

class InfobarUIChangeObserver : public TabStripModelObserver {
 public:
  explicit InfobarUIChangeObserver(BrowserWindowInterface* browser)
      : browser_{browser} {
    for (int tab = 0; tab < browser_->GetTabStripModel()->count(); ++tab) {
      auto* contents = browser_->GetTabStripModel()->GetWebContentsAt(tab);
      observers_[contents] =
          std::make_unique<InfoBarChangeObserver>(base::BindOnce(
              &InfobarUIChangeObserver::EraseObserver, base::Unretained(this)));
      GetInfoBarManager(contents)->AddObserver(observers_[contents].get());
    }
    browser_->GetTabStripModel()->AddObserver(this);
  }

  ~InfobarUIChangeObserver() override {
    for (auto& observer_iter : observers_) {
      auto* contents = observer_iter.first;
      auto* observer = observer_iter.second.get();

      GetInfoBarManager(contents)->RemoveObserver(observer);
    }
    browser_->GetTabStripModel()->RemoveObserver(this);
    observers_.clear();
  }

  void ExpectCalls(size_t expected_changes) {
    run_loop_ = std::make_unique<base::RunLoop>();
    barrier_closure_ =
        base::BarrierClosure(expected_changes, run_loop_->QuitClosure());
    for (auto& observer : observers_) {
      observer.second->SetCallback(barrier_closure_);
    }
  }

  void Wait() { run_loop_->Run(); }

  // TabStripModelObserver
  void OnTabStripModelChanged(
      TabStripModel* tab_strip_model,
      const TabStripModelChange& change,
      const TabStripSelectionChange& selection) override {
    if (change.type() == TabStripModelChange::kInserted) {
      for (const auto& contents_with_index : change.GetInsert()->contents) {
        auto* contents = contents_with_index.contents.get();
        if (observers_.find(contents) == observers_.end()) {
          observers_[contents] = std::make_unique<InfoBarChangeObserver>(
              base::BindOnce(&InfobarUIChangeObserver::EraseObserver,
                             base::Unretained(this)));
          GetInfoBarManager(contents)->AddObserver(observers_[contents].get());
          if (!barrier_closure_.is_null()) {
            observers_[contents]->SetCallback(barrier_closure_);
          }
        }
      }
    }
  }
  void OnTabChangedAt(tabs::TabInterface* tab,
                      TabChangeType change_type) override {
    if (observers_.find(tab->GetContents()) == observers_.end()) {
      observers_[tab->GetContents()] =
          std::make_unique<InfoBarChangeObserver>(base::BindOnce(
              &InfobarUIChangeObserver::EraseObserver, base::Unretained(this)));
      GetInfoBarManager(tab->GetContents())
          ->AddObserver(observers_[tab->GetContents()].get());
      if (!barrier_closure_.is_null()) {
        observers_[tab->GetContents()]->SetCallback(barrier_closure_);
      }
    }
  }

 private:
  class InfoBarChangeObserver;

 public:
  void EraseObserver(InfoBarChangeObserver* observer) {
    auto iter = std::ranges::find(
        observers_, observer,
        [](const auto& observer_iter) { return observer_iter.second.get(); });
    observers_.erase(iter);
  }

 private:
  class InfoBarChangeObserver : public infobars::InfoBarManager::Observer {
   public:
    using ShutdownCallback = base::OnceCallback<void(InfoBarChangeObserver*)>;

    explicit InfoBarChangeObserver(ShutdownCallback shutdown_callback)
        : shutdown_callback_{std::move(shutdown_callback)} {}

    ~InfoBarChangeObserver() override = default;

    void SetCallback(base::RepeatingClosure change_closure) {
      DCHECK(!change_closure.is_null());
      change_closure_ = change_closure;
    }

    void OnInfoBarAdded(infobars::InfoBar* infobar) override {
      DCHECK(!change_closure_.is_null());
      change_closure_.Run();
    }

    void OnInfoBarRemoved(infobars::InfoBar* infobar, bool animate) override {
      DCHECK(!change_closure_.is_null());
      change_closure_.Run();
    }

    void OnInfoBarReplaced(infobars::InfoBar* old_infobar,
                           infobars::InfoBar* new_infobar) override {
      NOTREACHED();
    }

    void OnManagerWillBeDestroyed(infobars::InfoBarManager* manager) override {
      manager->RemoveObserver(this);
      DCHECK(!shutdown_callback_.is_null());
      std::move(shutdown_callback_).Run(this);
    }

   private:
    base::RepeatingClosure change_closure_;
    ShutdownCallback shutdown_callback_;
  };

  std::unique_ptr<base::RunLoop> run_loop_;
  std::map<content::WebContents*, std::unique_ptr<InfoBarChangeObserver>>
      observers_;
  raw_ptr<BrowserWindowInterface> browser_;
  base::RepeatingClosure barrier_closure_;
};

}  // namespace

// Top-level integration test for WebRTC. Uses an actual desktop capture
// extension to capture the whole screen or a tab.
class WebRtcDesktopCaptureBrowserTest : public WebRtcTestBase {
 public:
  using MediaIDCallback = base::OnceCallback<content::DesktopMediaID()>;

  WebRtcDesktopCaptureBrowserTest() {
    extensions::DesktopCaptureChooseDesktopMediaFunction::
        SetPickerFactoryForTests(&picker_factory_);
  }

  void SetUpInProcessBrowserTestFixture() override {
    DetectErrorsInJavaScript();  // Look for errors in our rather complex js.
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Ensure the infobar is enabled, since we expect that in this test.
    EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream));

    // Flags use to automatically select the right dekstop source and get
    // around security restrictions.
    command_line->AppendSwitchASCII(switches::kAutoSelectDesktopCaptureSource,
                                    "Entire screen");
    command_line->AppendSwitch(switches::kEnableUserMediaScreenCapturing);
    // MSan and GL do not get along so avoid using the GPU with MSan.
    // TODO(crbug.com/40260482): Remove this after fixing feature
    // detection in 0c tab capture path as it'll no longer be needed.
#if !BUILDFLAG(IS_CHROMEOS) && !defined(MEMORY_SANITIZER)
    command_line->AppendSwitch(switches::kUseGpuInTests);
#endif
  }

 protected:
  void InitializeTabSharingForFirstTab(
      MediaIDCallback media_id_callback,
      InfobarUIChangeObserver* observer,
      std::optional<std::string> extra_video_constraints = std::nullopt) {
    ASSERT_TRUE(embedded_test_server()->Start());
    LoadDesktopCaptureExtension();
    auto* first_tab = OpenTestPageInNewTab(kMainWebrtcTestHtmlPage);
    OpenTestPageInNewTab(kMainWebrtcTestHtmlPage);

    FakeDesktopMediaPickerFactory::TestFlags test_flags{
        .expect_screens = true,
        .expect_windows = true,
        .expect_tabs = true,
        .picker_result = std::move(media_id_callback).Run(),
    };
    picker_factory_.SetTestFlags(base::span_from_ref(test_flags));

    std::string stream_id = GetDesktopMediaStream(first_tab);
    EXPECT_NE(stream_id, "");

    LOG(INFO) << "Opened desktop media stream, got id " << stream_id;

    std::string constraints = base::StrCat(
        {"{audio: false, video: { mandatory: {chromeMediaSource: 'desktop', "
         "chromeMediaSourceId: '",
         stream_id, "'", (extra_video_constraints.has_value() ? ", " : ""),
         extra_video_constraints.value_or(""), "}}}"});

    // Should create 3 infobars if a tab (webcontents) is shared!
    if (observer)
      observer->ExpectCalls(3);

    EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAcceptIfPrompted(
        first_tab, constraints));

    if (observer)
      observer->Wait();
  }

  void DetectVideoAndHangUp(content::WebContents* first_tab,
                            content::WebContents* second_tab) {
    StartDetectingVideo(first_tab, "remote-view");
    StartDetectingVideo(second_tab, "remote-view");
#if !BUILDFLAG(IS_MAC)
    // Video is choppy on Mac OS X. http://crbug.com/40398907.
    WaitForVideoToPlay(first_tab);
    WaitForVideoToPlay(second_tab);
#endif
    HangUp(first_tab);
    HangUp(second_tab);
  }

  void RunP2PScreenshareWhileSharing(MediaIDCallback media_id_callback) {
    InitializeTabSharingForFirstTab(std::move(media_id_callback), nullptr);
    auto* first_tab = browser()->tab_strip_model()->GetWebContentsAt(1);
    auto* second_tab = browser()->tab_strip_model()->GetWebContentsAt(2);
    GetUserMediaAndAccept(second_tab);

    SetupPeerconnectionWithLocalStream(first_tab);
    SetupPeerconnectionWithLocalStream(second_tab);
    NegotiateCall(first_tab, second_tab);
    DetectVideoAndHangUp(first_tab, second_tab);
  }

  FakeDesktopMediaPickerFactory picker_factory_;

  // TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
  extensions::ScopedTestMV2Enabler mv2_enabler_;
};

// TODO(crbug.com/40915051): Fails on MAC.
// TODO(crbug.com/40915051): Fails with MSAN. Determine if enabling the test for
// MSAN is feasible or not.
// TODO(crbug.com/479691925): Fails on Windows 11.
#if BUILDFLAG(IS_MAC)
#define MAYBE_TabCaptureProvidesMinFps DISABLED_TabCaptureProvidesMinFps
#elif defined(MEMORY_SANITIZER)
#define MAYBE_TabCaptureProvidesMinFps DISABLED_TabCaptureProvidesMinFps
#elif BUILDFLAG(IS_WIN)
#define MAYBE_TabCaptureProvidesMinFps DISABLED_TabCaptureProvidesMinFps
#else
#define MAYBE_TabCaptureProvidesMinFps TabCaptureProvidesMinFps
#endif
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
                       MAYBE_TabCaptureProvidesMinFps) {
  constexpr int kFps = 30;
  constexpr const char* kFpsString = "30";
  constexpr int kTestTimeSeconds = 2;
  // We wait with measuring frame rate until a few frames has passed. This is
  // because the frame rate frame dropper in VideoTrackAdapter is pretty
  // aggressive dropping frames when the stream starts.
  constexpr int kNumFramesBeforeStabilization = kFps;

  InitializeTabSharingForFirstTab(
      base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 1),
      nullptr, base::StrCat({"minFrameRate: ", kFpsString}));
  content::WebContents* first_tab =
      browser()->tab_strip_model()->GetWebContentsAt(1);
  EnableVideoFrameCallbacks(first_tab, "local-view");

  // First wait for a frame to appear, then wait until we get the number of
  // frames expected during the test time.
  int initial_frame_counter = 0;
  base::TimeTicks initial_timestamp;
  ASSERT_TRUE(test::PollingWaitUntilClosureEvaluatesTrue(
      base::BindLambdaForTesting([&]() -> bool {
        initial_timestamp = base::TimeTicks::Now();
        initial_frame_counter = GetNumVideoFrameCallbacks(first_tab);
        return initial_frame_counter > kNumFramesBeforeStabilization;
      }),
      first_tab, base::Milliseconds(50)));
  int final_frame_counter = 0;
  base::TimeTicks final_timestamp;
  ASSERT_TRUE(test::PollingWaitUntilClosureEvaluatesTrue(
      base::BindLambdaForTesting([&]() -> bool {
        final_timestamp = base::TimeTicks::Now();
        final_frame_counter = GetNumVideoFrameCallbacks(first_tab);
        return final_frame_counter >=
               kTestTimeSeconds * kFps + initial_frame_counter;
      }),
      first_tab, base::Milliseconds(50)));
  int average_fps = (final_frame_counter - initial_frame_counter) * 1000 /
                    (final_timestamp - initial_timestamp).InMilliseconds();
  // MediaStreamVideoTrack upholds the min fps by way of an idle timer getting
  // reset for every received frame from the source. Sources being slow to
  // provide frames or plumbed main thread will ensure that the FPS provided is
  // actually always strictly lower than the requested minimum.
  // Expect at least 1/3 of the expected frames have appeared to aggressively
  // combat flakes.
  ASSERT_GE(average_fps, kFps / 3);
}

// TODO(crbug.com/40915051): Fails on Linux ASan, LSan and MSan builders.
#if BUILDFLAG(IS_LINUX) &&                                      \
    ((defined(ADDRESS_SANITIZER) && defined(LEAK_SANITIZER)) || \
     defined(MEMORY_SANITIZER))
#define MAYBE_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent \
  DISABLED_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent
#else
#define MAYBE_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent \
  TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent
#endif
IN_PROC_BROWSER_TEST_F(
    WebRtcDesktopCaptureBrowserTest,
    MAYBE_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent) {
  constexpr base::TimeDelta kTestTime = base::Seconds(2);
  InitializeTabSharingForFirstTab(
      base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 1),
      nullptr, base::StrCat({"minFrameRate: 0, maxFrameRate: 30"}));
  content::WebContents* first_tab =
      browser()->tab_strip_model()->GetWebContentsAt(1);
  EnableVideoFrameCallbacks(first_tab, "local-view");

  // Sample received frame counts during the test time.
  int frame_counter = 0;
  base::TimeTicks initial_timestamp = base::TimeTicks::Now();
  ASSERT_TRUE(test::PollingWaitUntilClosureEvaluatesTrue(
      base::BindLambdaForTesting([&]() -> bool {
        frame_counter = GetNumVideoFrameCallbacks(first_tab);
        return base::TimeTicks::Now() - initial_timestamp >= kTestTime;
      }),
      first_tab, base::Milliseconds(50)));
  // Expect only a few initial frames.
  ASSERT_LE(frame_counter, 3);
}

// Flaky on ASan bots. See https://crbug.com/40270173.
// Crashes on some Macs. See https://crbug.com/351095634.
#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_RunP2PScreenshareWhileSharingScreen \
  DISABLED_RunP2PScreenshareWhileSharingScreen
#else
#define MAYBE_RunP2PScreenshareWhileSharingScreen \
  RunP2PScreenshareWhileSharingScreen
#endif
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
                       MAYBE_RunP2PScreenshareWhileSharingScreen) {
  RunP2PScreenshareWhileSharing(base::BindOnce(GetDesktopMediaIDForScreen));
}

// Flaky on ASan bots. See https://crbug.com/40270173.
#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER)
#define MAYBE_RunP2PScreenshareWhileSharingTab \
  DISABLED_RunP2PScreenshareWhileSharingTab
#else
#define MAYBE_RunP2PScreenshareWhileSharingTab RunP2PScreenshareWhileSharingTab
#endif
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
                       MAYBE_RunP2PScreenshareWhileSharingTab) {
  RunP2PScreenshareWhileSharing(
      base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 2));
}

IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
                       SwitchSharedTabBackAndForth) {
  InfobarUIChangeObserver observer(browser());

  InitializeTabSharingForFirstTab(
      base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 2),
      &observer);

  // Should delete 3 infobars and create 3 new!
  observer.ExpectCalls(6);
  // Switch shared tab from 2 to 0.
  GetDelegate(browser(), 0)->ShareThisTabInstead();
  observer.Wait();

  // Should delete 3 infobars and create 3 new!
  observer.ExpectCalls(6);
  // Switch shared tab from 0 to 2.
  GetDelegate(browser(), 2)->ShareThisTabInstead();
  observer.Wait();
}

IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
                       CloseAndReopenNonSharedTab) {
  InfobarUIChangeObserver observer(browser());

  InitializeTabSharingForFirstTab(
      base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 2),
      &observer);

  // Should delete 1 infobar.
  observer.ExpectCalls(1);
  // Close non-shared and non-sharing, i.e., unrelated tab
  browser()->tab_strip_model()->CloseWebContentsAt(
      0, TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB);
  observer.Wait();

  // Should create 1 infobar.
  observer.ExpectCalls(1);
  // Restore tab
  chrome::RestoreTab(browser());
  observer.Wait();
}
