// Copyright 2022 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/chromeos/video_conference/video_conference_manager_client.h"

#include <string>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/system/video_conference/fake_video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "base/command_line.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "chrome/browser/ash/video_conference/video_conference_manager_ash.h"
#include "chrome/browser/chromeos/video_conference/video_conference_manager_client_common.h"
#include "chrome/browser/chromeos/video_conference/video_conference_media_listener.h"
#include "chrome/browser/chromeos/video_conference/video_conference_web_app.h"
#include "chrome/browser/ui/ash/main_extra_parts/chrome_browser_main_extra_parts_ash.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_activity_simulator.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_user_data.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace video_conference {

namespace {
constexpr char kTestURL1[] = "about:blank";
constexpr char kTestURL2[] = "https://localhost";
}  // namespace

// Fake class for testing `VideoConferenceManagerClientImpl`. Overrides
// `NotifyManager()` to not send any updates to the VC manager and provides
// access to the internal `id_to_webcontents` map on the client.
class FakeVideoConferenceManagerClient
    : public VideoConferenceManagerClientImpl {
 public:
  // The passed `vc_manager` must outlive this instance.
  explicit FakeVideoConferenceManagerClient(
      ash::VideoConferenceManagerAsh* vc_manager)
      : VideoConferenceManagerClientImpl(vc_manager) {}

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

  ~FakeVideoConferenceManagerClient() override = default;

  std::map<base::UnguessableToken, raw_ptr<content::WebContents>>
  id_to_webcontents() {
    return id_to_webcontents_;
  }

  const base::UnguessableToken& client_id() { return client_id_; }

  VideoConferencePermissions GetAggregatedPermissions() {
    return VideoConferenceManagerClientImpl::GetAggregatedPermissions();
  }

  bool camera_system_enabled() {
    return media_listener_->camera_system_enabled_;
  }

  bool microphone_system_enabled() {
    return media_listener_->microphone_system_enabled_;
  }

  ash::VideoConferenceMediaUsageStatus& status() { return status_; }

 protected:
  void NotifyManager(ash::VideoConferenceMediaUsageStatus status) override {}
};

class VideoConferenceManagerClientTest : public InProcessBrowserTest {
 public:
  VideoConferenceManagerClientTest() = default;

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

  ~VideoConferenceManagerClientTest() override = default;

  void SetUp() override {
    scoped_feature_list_.InitAndEnableFeature(
        ash::features::kFeatureManagementVideoConference);

    InProcessBrowserTest::SetUp();
  }

  // Creates and returns a new `WebContents` at the given tab `index`.
  content::WebContents* CreateWebContentsAt(int index) {
    EXPECT_TRUE(
        AddTabAtIndex(index, GURL(kTestURL1), ui::PAGE_TRANSITION_TYPED));
    return browser()->tab_strip_model()->GetWebContentsAt(index);
  }

  // Removed `WebContents` at the given tab `index`.
  void RemoveWebContentsAt(int index) {
    browser()->tab_strip_model()->CloseWebContentsAt(index,
                                                     TabCloseTypes::CLOSE_NONE);
  }

  void UpdateWebContentsTitle(content::WebContents* contents,
                              const std::u16string& title) {
    content::NavigationEntry* entry =
        contents->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    contents->UpdateTitleForEntry(entry, title);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests creating VcWebApps and removing them by closing tabs.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest,
                       TabCreationAndRemoval) {
  FakeVideoConferenceManagerClient client(
      ash::VideoConferenceManagerAsh::Get());

  auto* web_contents1 = CreateWebContentsAt(0);
  auto* web_contents2 = CreateWebContentsAt(1);
  auto* web_contents3 = CreateWebContentsAt(2);

  client.CreateVideoConferenceWebApp(web_contents1);
  EXPECT_EQ(client.id_to_webcontents().size(), 1u);

  client.CreateVideoConferenceWebApp(web_contents2);
  EXPECT_EQ(client.id_to_webcontents().size(), 2u);

  client.CreateVideoConferenceWebApp(web_contents3);
  EXPECT_EQ(client.id_to_webcontents().size(), 3u);

  // It's important to close tabs from right-to-left as otherwise the indices
  // change.
  RemoveWebContentsAt(2);
  EXPECT_EQ(client.id_to_webcontents().size(), 2u);

  RemoveWebContentsAt(1);
  EXPECT_EQ(client.id_to_webcontents().size(), 1u);

  RemoveWebContentsAt(0);
  EXPECT_EQ(client.id_to_webcontents().size(), 0u);
}

// Tests that a change in the primary page of the web contents of a VcWebApp
// removes it from the client.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest,
                       WebContentsPrimaryPageChange) {
  FakeVideoConferenceManagerClient client(
      ash::VideoConferenceManagerAsh::Get());
  TabActivitySimulator tab_activity_simulator;

  auto* web_contents = CreateWebContentsAt(0);
  auto* vc_app = client.CreateVideoConferenceWebApp(web_contents);

  EXPECT_EQ(client.id_to_webcontents().size(), 1u);

  // Ensure tab is in focus.
  vc_app->ActivateApp();
  // Navigate to a different URL and trigger a primary page change event.
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kTestURL2)));
  // There should no longer be a WebContentsUserData associated with this
  // `web_contents`.
  EXPECT_FALSE(
      content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
          web_contents));

  EXPECT_EQ(client.id_to_webcontents().size(), 0u);
}

// Tests `GetMediaApps` returns `VideoConferenceMediaAppInfo`s with expected
// values.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest, GetMediaApps) {
  FakeVideoConferenceManagerClient client(
      ash::VideoConferenceManagerAsh::Get());

  auto* web_contents1 = CreateWebContentsAt(0);
  UpdateWebContentsTitle(web_contents1, u"app1");

  auto* web_contents2 = CreateWebContentsAt(1);
  UpdateWebContentsTitle(web_contents2, u"app2");

  auto* web_contents3 = CreateWebContentsAt(2);
  auto* vc_app1 = client.CreateVideoConferenceWebApp(web_contents1);
  auto* vc_app2 = client.CreateVideoConferenceWebApp(web_contents2);
  auto* vc_app3 = client.CreateVideoConferenceWebApp(web_contents3);

  vc_app1->state().is_capturing_camera = true;

  vc_app1->state().is_capturing_microphone = true;
  vc_app2->state().is_capturing_microphone = true;

  vc_app1->state().is_capturing_screen = true;
  vc_app2->state().is_capturing_screen = true;
  vc_app3->state().is_capturing_screen = true;

  std::map<base::UnguessableToken, VideoConferenceWebApp*> id_to_vc_app = {
      {vc_app1->state().id, vc_app1},
      {vc_app2->state().id, vc_app2},
      {vc_app3->state().id, vc_app3},
  };

  auto apps = client.GetMediaApps();
  EXPECT_EQ(apps.size(), 3u);

  for (auto& app : apps) {
    auto* vc_app = id_to_vc_app[app.id];
    EXPECT_EQ(vc_app->state().is_capturing_camera, app.is_capturing_camera);
    EXPECT_EQ(vc_app->state().is_capturing_microphone,
              app.is_capturing_microphone);
    EXPECT_EQ(vc_app->state().is_capturing_screen, app.is_capturing_screen);
    EXPECT_EQ(vc_app->GetWebContents().GetTitle(), app.title);
  }
}

// Tests setting/clearing system statuses for camera and microphone.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest,
                       SetSystemMediaDeviceStatus) {
  auto* vc_manager = ash::VideoConferenceManagerAsh::Get();
  FakeVideoConferenceManagerClient client(vc_manager);

  vc_manager->RegisterCppClient(&client, client.client_id());

  ash::FakeVideoConferenceTrayController* controller =
      static_cast<ash::FakeVideoConferenceTrayController*>(
          ash::VideoConferenceTrayController::Get());
  ASSERT_TRUE(controller);
  EXPECT_EQ(controller->device_used_while_disabled_records().size(), 0u);

  EXPECT_TRUE(client.camera_system_enabled());
  EXPECT_TRUE(client.microphone_system_enabled());

  vc_manager->SetSystemMediaDeviceStatus(
      ash::VideoConferenceMediaDevice::kCamera,
      /*enabled=*/false);
  EXPECT_FALSE(client.camera_system_enabled());
  EXPECT_TRUE(client.microphone_system_enabled());

  vc_manager->SetSystemMediaDeviceStatus(
      ash::VideoConferenceMediaDevice::kMicrophone,
      /*enabled=*/false);
  EXPECT_FALSE(client.camera_system_enabled());
  EXPECT_FALSE(client.microphone_system_enabled());

  vc_manager->SetSystemMediaDeviceStatus(
      ash::VideoConferenceMediaDevice::kMicrophone,
      /*enabled=*/true);
  EXPECT_FALSE(client.camera_system_enabled());
  EXPECT_TRUE(client.microphone_system_enabled());

  vc_manager->SetSystemMediaDeviceStatus(
      ash::VideoConferenceMediaDevice::kCamera,
      /*enabled=*/true);
  EXPECT_TRUE(client.camera_system_enabled());
  EXPECT_TRUE(client.microphone_system_enabled());
}

// Tests client updates relating to adding and removing VC web apps and title
// changes.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest, ClientUpdate) {
  auto* vc_manager = ash::VideoConferenceManagerAsh::Get();
  FakeVideoConferenceManagerClient client(vc_manager);

  vc_manager->RegisterCppClient(&client, client.client_id());

  ash::FakeVideoConferenceTrayController* controller =
      static_cast<ash::FakeVideoConferenceTrayController*>(
          ash::VideoConferenceTrayController::Get());
  ASSERT_TRUE(controller);

  // Add a new VC web app on the client.
  EXPECT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK));
  auto* web_contents = browser()->tab_strip_model()->GetWebContentsAt(0);
  client.CreateVideoConferenceWebApp(web_contents);

  // Confirm the update received by the controller has `added_or_removed_app`
  // set to true.
  EXPECT_EQ(controller->last_client_update().added_or_removed_app,
            ash::VideoConferenceAppUpdate::kAppAdded);
  EXPECT_FALSE(controller->last_client_update().title_change_info);

  // Update the title and confirm correct fields were set.
  std::u16string new_title = u"New Title";
  UpdateWebContentsTitle(web_contents, new_title);

  EXPECT_EQ(controller->last_client_update().added_or_removed_app,
            ash::VideoConferenceAppUpdate::kNone);
  EXPECT_TRUE(controller->last_client_update().title_change_info);
  EXPECT_EQ(controller->last_client_update().title_change_info->new_title,
            new_title);

  // Remove the VC web app by closing the corresponding WebContents.
  browser()->tab_strip_model()->CloseWebContentsAt(0,
                                                   TabCloseTypes::CLOSE_NONE);

  EXPECT_EQ(controller->last_client_update().added_or_removed_app,
            ash::VideoConferenceAppUpdate::kAppRemoved);
  EXPECT_FALSE(controller->last_client_update().title_change_info);
}

// Tests aggregated media usage status received on `HandleMediaUsageUpdate`.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest, MediaUsageUpdate) {
  FakeVideoConferenceManagerClient client(
      ash::VideoConferenceManagerAsh::Get());

  EXPECT_FALSE(client.status().state.has_media_app);
  EXPECT_FALSE(client.status().state.is_capturing_camera);
  EXPECT_FALSE(client.status().state.is_capturing_microphone);
  EXPECT_FALSE(client.status().state.is_capturing_screen);

  auto* web_contents1 = CreateWebContentsAt(0);
  UpdateWebContentsTitle(web_contents1, u"app1");

  auto* web_contents2 = CreateWebContentsAt(1);
  UpdateWebContentsTitle(web_contents2, u"app2");

  auto* web_contents3 = CreateWebContentsAt(2);
  auto* vc_app1 = client.CreateVideoConferenceWebApp(web_contents1);
  auto* vc_app2 = client.CreateVideoConferenceWebApp(web_contents2);
  auto* vc_app3 = client.CreateVideoConferenceWebApp(web_contents3);

  client.HandleMediaUsageUpdate();
  EXPECT_TRUE(client.status().state.has_media_app);
  EXPECT_FALSE(client.status().state.is_capturing_camera);
  EXPECT_FALSE(client.status().state.is_capturing_microphone);
  EXPECT_FALSE(client.status().state.is_capturing_screen);

  vc_app1->state().is_capturing_camera = true;
  client.HandleMediaUsageUpdate();
  EXPECT_TRUE(client.status().state.has_media_app);
  EXPECT_TRUE(client.status().state.is_capturing_camera);
  EXPECT_FALSE(client.status().state.is_capturing_microphone);
  EXPECT_FALSE(client.status().state.is_capturing_screen);

  vc_app2->state().is_capturing_microphone = true;
  client.HandleMediaUsageUpdate();
  EXPECT_TRUE(client.status().state.has_media_app);
  EXPECT_TRUE(client.status().state.is_capturing_camera);
  EXPECT_TRUE(client.status().state.is_capturing_microphone);
  EXPECT_FALSE(client.status().state.is_capturing_screen);

  vc_app3->state().is_capturing_screen = true;
  client.HandleMediaUsageUpdate();
  EXPECT_TRUE(client.status().state.has_media_app);
  EXPECT_TRUE(client.status().state.is_capturing_camera);
  EXPECT_TRUE(client.status().state.is_capturing_microphone);
  EXPECT_TRUE(client.status().state.is_capturing_screen);

  RemoveWebContentsAt(2);
  RemoveWebContentsAt(1);
  RemoveWebContentsAt(0);

  client.HandleMediaUsageUpdate();
  EXPECT_FALSE(client.status().state.has_media_app);
  EXPECT_FALSE(client.status().state.is_capturing_camera);
  EXPECT_FALSE(client.status().state.is_capturing_microphone);
  EXPECT_FALSE(client.status().state.is_capturing_screen);
}

// Tests if `ReturnToApp` correctly activates tab of the `VideoConferenceWebApp`
// corresponding to the `id` provided.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest, ReturnToApp) {
  FakeVideoConferenceManagerClient client(
      ash::VideoConferenceManagerAsh::Get());

  auto* web_contents1 = CreateWebContentsAt(0);
  auto* web_contents2 = CreateWebContentsAt(1);

  auto* vc_app1 = client.CreateVideoConferenceWebApp(web_contents1);
  auto* vc_app2 = client.CreateVideoConferenceWebApp(web_contents2);

  EXPECT_TRUE(client.ReturnToApp(vc_app1->state().id));
  EXPECT_EQ(browser()->tab_strip_model()->active_index(), 0);

  EXPECT_TRUE(client.ReturnToApp(vc_app2->state().id));
  EXPECT_EQ(browser()->tab_strip_model()->active_index(), 1);

  EXPECT_TRUE(client.ReturnToApp(vc_app1->state().id));
  EXPECT_EQ(browser()->tab_strip_model()->active_index(), 0);
}

// Tests that for extensions, permissions equate to capturing statuses.
IN_PROC_BROWSER_TEST_F(VideoConferenceManagerClientTest, ExtensionPermissions) {
  FakeVideoConferenceManagerClient client(
      ash::VideoConferenceManagerAsh::Get());

  auto* web_contents = CreateWebContentsAt(0);

  auto* vc_app = client.CreateVideoConferenceWebApp(web_contents);

  // Make vc_app an extension.
  vc_app->state().is_extension = true;

  auto permissions = client.GetAggregatedPermissions();
  EXPECT_FALSE(permissions.has_camera_permission);
  EXPECT_FALSE(permissions.has_microphone_permission);

  // Set capturing to true.
  vc_app->state().is_capturing_camera = true;

  permissions = client.GetAggregatedPermissions();
  EXPECT_TRUE(permissions.has_camera_permission);
  EXPECT_FALSE(permissions.has_microphone_permission);

  // Make vc_app a non-extension.
  vc_app->state().is_extension = false;

  permissions = client.GetAggregatedPermissions();
  EXPECT_FALSE(permissions.has_camera_permission);
  EXPECT_FALSE(permissions.has_microphone_permission);
}

}  // namespace video_conference
