// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/media/webrtc/tab_desktop_media_list.h"

#include <memory>

#include "base/command_line.h"
#include "base/containers/cxx20_erase.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/gmock_callback_support.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/media/webrtc/desktop_media_list.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/apps/chrome_app_delegate.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/fake_profile_manager.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/test_browser_window.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/app_window/test_app_window_contents.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/value_builder.h"
#include "testing/gmock/include/gmock/gmock.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/login/users/scoped_test_user_manager.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

using content::WebContents;
using content::WebContentsTester;

namespace {

constexpr int kDefaultSourceCount = 2;
constexpr int kThumbnailSize = 50;

// Create a greyscale image with certain size and grayscale value.
gfx::Image CreateGrayscaleImage(gfx::Size size, uint8_t greyscale_value) {
  SkBitmap result;
  result.allocN32Pixels(size.width(), size.height(), true);

  uint8_t* pixels_data = reinterpret_cast<uint8_t*>(result.getPixels());

  // Set greyscale value for all pixels.
  for (int y = 0; y < result.height(); ++y) {
    for (int x = 0; x < result.width(); ++x) {
      pixels_data[result.rowBytes() * y + x * result.bytesPerPixel()] =
          greyscale_value;
      pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 1] =
          greyscale_value;
      pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 2] =
          greyscale_value;
      pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 3] =
          0xff;
    }
  }

  return gfx::Image::CreateFrom1xBitmap(result);
}

}  // namespace

class MockObserver : public DesktopMediaListObserver {
 public:
  MOCK_METHOD1(OnSourceAdded, void(int index));
  MOCK_METHOD1(OnSourceRemoved, void(int index));
  MOCK_METHOD2(OnSourceMoved, void(int old_index, int new_index));
  MOCK_METHOD1(OnSourceNameChanged, void(int index));
  MOCK_METHOD1(OnSourceThumbnailChanged, void(int index));
  MOCK_METHOD1(OnSourcePreviewChanged, void(size_t index));

  void VerifyAndClearExpectations() {
    testing::Mock::VerifyAndClearExpectations(this);
  }
};

ACTION_P2(CheckListSize, list, expected_list_size) {
  EXPECT_EQ(expected_list_size, list->GetSourceCount());
}

ACTION(QuitMessageLoop) {
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::RunLoop::QuitCurrentWhenIdleClosureDeprecated());
}

// This is a helper class to abstract away some of the details of creating and
// managing the life-cycle of an AppWindow
class TestAppWindow : public content::WebContentsObserver {
 public:
  TestAppWindow(const TestAppWindow&) = delete;
  TestAppWindow& operator=(const TestAppWindow&) = delete;

  TestAppWindow(Profile* profile,
                const extensions::Extension* extension,
                std::unique_ptr<content::WebContents> contents) {
    window_ = new extensions::AppWindow(
        profile, new ChromeAppDelegate(profile, false), extension);
    window_->SetAppWindowContentsForTesting(
        std::make_unique<extensions::TestAppWindowContents>(
            std::move(contents)));

    extensions::AppWindowRegistry::Get(profile)->AddAppWindow(window_);
    Observe(window_->web_contents());
  }

  ~TestAppWindow() override { Close(); }

  void Close() {
    if (!window_)
      return;

    content::WebContentsDestroyedWatcher destroyed_watcher(
        window_->web_contents());
    window_->OnNativeClose();
    destroyed_watcher.Wait();

    EXPECT_FALSE(window_);
  }

  void WebContentsDestroyed() override { window_ = nullptr; }

 private:
  extensions::AppWindow* window_;
};

class TabDesktopMediaListTest : public testing::Test {
 public:
  TabDesktopMediaListTest(const TabDesktopMediaListTest&) = delete;
  TabDesktopMediaListTest& operator=(const TabDesktopMediaListTest&) = delete;

 protected:
  TabDesktopMediaListTest()
      : local_state_(TestingBrowserProcess::GetGlobal()) {}

  std::unique_ptr<content::WebContents> CreateWebContents(
      int favicon_greyscale) {
    std::unique_ptr<WebContents> contents(
        content::WebContentsTester::CreateTestWebContents(
            profile_, content::SiteInstance::Create(profile_)));
    if (!contents)
      return nullptr;

    WebContentsTester::For(contents.get())
        ->SetLastActiveTime(base::TimeTicks::Now());

    // Get or create a NavigationEntry and add a title and a favicon to it.
    content::NavigationEntry* entry =
        contents->GetController().GetLastCommittedEntry();
    if (!entry) {
      content::NavigationSimulator::NavigateAndCommitFromBrowser(
          contents.get(), GURL("chrome://blank"));
      entry = contents->GetController().GetLastCommittedEntry();
    }

    contents->UpdateTitleForEntry(entry, u"Test tab");

    content::FaviconStatus favicon_info;
    favicon_info.image =
        CreateGrayscaleImage(gfx::Size(10, 10), favicon_greyscale);
    entry->GetFavicon() = favicon_info;

    return contents;
  }

  void AddWebcontents(int favicon_greyscale) {
    TabStripModel* tab_strip_model = browser_->tab_strip_model();
    ASSERT_TRUE(tab_strip_model);
    auto contents = CreateWebContents(favicon_greyscale);
    ASSERT_TRUE(contents);
    manually_added_web_contents_.push_back(contents.get());
    tab_strip_model->AppendWebContents(std::move(contents), true);
  }

  const extensions::Extension* BuildOrGetExtension() {
    if (!extension_) {
      extension_ =
          extensions::ExtensionBuilder()
              .SetManifest(extensions::DictionaryBuilder()
                               .Set("name", "TabListUnitTest Extension")
                               .Set("version", "1.0")
                               .Set("manifest_version", 2)
                               .Build())
              .Build();
    }
    return extension_.get();
  }

  void AddAppWindow() {
    auto app_window = std::make_unique<TestAppWindow>(
        profile_, BuildOrGetExtension(), CreateWebContents(10));

    manually_added_app_windows_.push_back(std::move(app_window));
  }

  void SetUp() override {
    rvh_test_enabler_ = std::make_unique<content::RenderViewHostTestEnabler>();
    // Create a new temporary directory, and store the path.
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    TestingBrowserProcess::GetGlobal()->SetProfileManager(
        std::make_unique<FakeProfileManager>(temp_dir_.GetPath()));

#if BUILDFLAG(IS_CHROMEOS_ASH)
    base::CommandLine* cl = base::CommandLine::ForCurrentProcess();
    cl->AppendSwitch(switches::kTestType);
#endif

    // Create profile.
    ProfileManager* profile_manager = g_browser_process->profile_manager();
    ASSERT_TRUE(profile_manager);

    profile_ = profile_manager->GetLastUsedProfileAllowedByPolicy();
    ASSERT_TRUE(profile_);

    // Create browser.
    Browser::CreateParams profile_params(profile_, true);
    browser_ = CreateBrowserWithTestWindowForParams(profile_params);
    ASSERT_TRUE(browser_);
    for (int i = 0; i < kDefaultSourceCount; i++) {
      AddWebcontents(i + 1);
    }
  }

  void TearDown() override {
    list_.reset();

    // TODO(erikchen): Tearing down the TabStripModel should just delete all its
    // owned WebContents. Then |manually_added_web_contents_| won't be
    // necessary. https://crbug.com/832879.
    TabStripModel* tab_strip_model = browser_->tab_strip_model();
    for (WebContents* contents : manually_added_web_contents_) {
      tab_strip_model->DetachAndDeleteWebContentsAt(
          tab_strip_model->GetIndexOfWebContents(contents));
    }
    manually_added_web_contents_.clear();
    manually_added_app_windows_.clear();

    browser_.reset();
    TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr);
    base::RunLoop().RunUntilIdle();
    rvh_test_enabler_.reset();
  }

  void CreateDefaultList() {
    // The actual "default" for |include_chrome_app_windows| is false; but for
    // the purposes of the tests we make the default true, so that all paths are
    // exercised.
    list_ = std::make_unique<TabDesktopMediaList>(
        base::BindRepeating(
            [](content::WebContents* contents) { return true; }),
        /*include_chrome_app_windows=*/true);
    list_->SetThumbnailSize(gfx::Size(kThumbnailSize, kThumbnailSize));

    // Set update period to reduce the time it takes to run tests.
    // >0 to avoid unit test failure.
    list_->SetUpdatePeriod(base::Milliseconds(1));
  }

  void InitializeAndVerify() {
    CreateDefaultList();

    // The tabs in media source list are sorted in decreasing time order. The
    // latest one is listed first. However, tabs are added to TabStripModel in
    // increasing time order, the oldest one is added first.
    {
      testing::InSequence dummy;

      for (int i = 0; i < kDefaultSourceCount; i++) {
        EXPECT_CALL(observer_, OnSourceAdded(i))
            .WillOnce(CheckListSize(list_.get(), i + 1));
      }

      for (int i = 0; i < kDefaultSourceCount - 1; i++) {
        EXPECT_CALL(observer_,
                    OnSourceThumbnailChanged(kDefaultSourceCount - 1 - i));
      }
      EXPECT_CALL(observer_, OnSourceThumbnailChanged(0))
          .WillOnce(QuitMessageLoop());
    }

    list_->StartUpdating(&observer_);
    base::RunLoop().Run();

    for (int i = 0; i < kDefaultSourceCount; ++i) {
      EXPECT_EQ(list_->GetSource(i).id.type,
                content::DesktopMediaID::TYPE_WEB_CONTENTS);
    }

    observer_.VerifyAndClearExpectations();
  }

  // The path to temporary directory used to contain the test operations.
  base::ScopedTempDir temp_dir_;
  ScopedTestingLocalState local_state_;

  std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_;
  Profile* profile_;
  std::unique_ptr<Browser> browser_;

  // Must be listed before |list_|, so it's destroyed last.
  MockObserver observer_;
  std::unique_ptr<TabDesktopMediaList> list_;
  std::vector<WebContents*> manually_added_web_contents_;
  std::vector<std::unique_ptr<TestAppWindow>> manually_added_app_windows_;

  content::BrowserTaskEnvironment task_environment_;

  scoped_refptr<const extensions::Extension> extension_;

#if BUILDFLAG(IS_CHROMEOS_ASH)
  ash::ScopedCrosSettingsTestHelper cros_settings_test_helper_;
  ash::ScopedTestUserManager test_user_manager_;
#endif
};

TEST_F(TabDesktopMediaListTest, AddTab) {
  InitializeAndVerify();

  AddWebcontents(10);

  EXPECT_CALL(observer_, OnSourceAdded(0))
      .WillOnce(CheckListSize(list_.get(), kDefaultSourceCount + 1));
  EXPECT_CALL(observer_, OnSourceThumbnailChanged(0))
      .WillOnce(QuitMessageLoop());

  base::RunLoop().Run();
}

TEST_F(TabDesktopMediaListTest, AddAppWindow) {
  InitializeAndVerify();

  AddAppWindow();

  base::RunLoop loop;
  // Note that unlike adding a tab, our AppWindow that we add is only
  // initialized enough to show up in the list; it doesn't have a favicon driver
  // which would be needed to extract the favicon from it.
  EXPECT_CALL(observer_, OnSourceAdded(0))
      .WillOnce(
          testing::DoAll(CheckListSize(list_.get(), kDefaultSourceCount + 1),
                         base::test::RunClosure(loop.QuitClosure())));

  loop.Run();
}

TEST_F(TabDesktopMediaListTest, RemoveTab) {
  InitializeAndVerify();

  TabStripModel* tab_strip_model = browser_->tab_strip_model();
  ASSERT_TRUE(tab_strip_model);
  std::unique_ptr<WebContents> released_web_contents =
      tab_strip_model->DetachWebContentsAtForInsertion(kDefaultSourceCount - 1);
  base::Erase(manually_added_web_contents_, released_web_contents.get());

  EXPECT_CALL(observer_, OnSourceRemoved(0))
      .WillOnce(
          testing::DoAll(CheckListSize(list_.get(), kDefaultSourceCount - 1),
                         QuitMessageLoop()));

  base::RunLoop().Run();
}

TEST_F(TabDesktopMediaListTest, MoveTab) {
  InitializeAndVerify();

  // Swap the two media sources by swap their time stamps.
  TabStripModel* tab_strip_model = browser_->tab_strip_model();
  ASSERT_TRUE(tab_strip_model);

  WebContents* contents0 = tab_strip_model->GetWebContentsAt(0);
  ASSERT_TRUE(contents0);
  base::TimeTicks t0 = contents0->GetLastActiveTime();
  WebContents* contents1 = tab_strip_model->GetWebContentsAt(1);
  ASSERT_TRUE(contents1);
  base::TimeTicks t1 = contents1->GetLastActiveTime();

  WebContentsTester::For(contents0)->SetLastActiveTime(t1);
  WebContentsTester::For(contents1)->SetLastActiveTime(t0);

  EXPECT_CALL(observer_, OnSourceMoved(1, 0))
      .WillOnce(testing::DoAll(CheckListSize(list_.get(), kDefaultSourceCount),
                               QuitMessageLoop()));

  base::RunLoop().Run();
}

TEST_F(TabDesktopMediaListTest, UpdateTitle) {
  InitializeAndVerify();

  // Change tab's title.
  TabStripModel* tab_strip_model = browser_->tab_strip_model();
  ASSERT_TRUE(tab_strip_model);
  WebContents* contents =
      tab_strip_model->GetWebContentsAt(kDefaultSourceCount - 1);
  ASSERT_TRUE(contents);
  content::NavigationController& controller = contents->GetController();
  contents->UpdateTitleForEntry(controller.GetLastCommittedEntry(),
                                u"New test tab");

  EXPECT_CALL(observer_, OnSourceNameChanged(0)).WillOnce(QuitMessageLoop());

  base::RunLoop().Run();

  EXPECT_EQ(list_->GetSource(0).name, u"New test tab");
}

TEST_F(TabDesktopMediaListTest, UpdateThumbnail) {
  InitializeAndVerify();

  // Change tab's favicon.
  TabStripModel* tab_strip_model = browser_->tab_strip_model();
  ASSERT_TRUE(tab_strip_model);
  WebContents* contents =
      tab_strip_model->GetWebContentsAt(kDefaultSourceCount - 1);
  ASSERT_TRUE(contents);

  content::FaviconStatus favicon_info;
  favicon_info.image = CreateGrayscaleImage(gfx::Size(10, 10), 100);
  contents->GetController().GetLastCommittedEntry()->GetFavicon() =
      favicon_info;

  EXPECT_CALL(observer_, OnSourceThumbnailChanged(0))
      .WillOnce(QuitMessageLoop());

  base::RunLoop().Run();
}

// Test that a source which is set as the one being previewed is marked as being
// visibly captured, so that it is still painted even when hidden.
TEST_F(TabDesktopMediaListTest, SetPreviewMarksTabAsVisiblyCaptured) {
  InitializeAndVerify();

  TabStripModel* tab_strip_model = browser_->tab_strip_model();
  ASSERT_TRUE(tab_strip_model);
  WebContents* contents =
      tab_strip_model->GetWebContentsAt(kDefaultSourceCount - 1);
  ASSERT_TRUE(contents);

  list_->SetPreviewedSource(list_->GetSource(0).id);

  EXPECT_TRUE(contents->IsBeingVisiblyCaptured());

  EXPECT_CALL(observer_, OnSourcePreviewChanged(0)).WillOnce(QuitMessageLoop());

  base::RunLoop().Run();
}
