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

#include <stdint.h>

#include "base/auto_reset.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/json/json_writer.h"
#include "base/numerics/safe_conversions.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/browser_app_launcher.h"
#include "chrome/browser/apps/platform_apps/api/media_galleries/media_galleries_api.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/media_galleries/media_file_system_registry.h"
#include "chrome/browser/media_galleries/media_galleries_preferences.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_paths.h"
#include "components/services/app_service/public/cpp/app_launch_params.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/storage_monitor/storage_info.h"
#include "components/storage_monitor/storage_monitor.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/process_manager.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/test/result_catcher.h"
#include "media/base/test_data_util.h"
#include "media/media_buildflags.h"

using extensions::PlatformAppBrowserTest;
using storage_monitor::StorageInfo;
using storage_monitor::StorageMonitor;

namespace {

// Dummy device properties.
const char kDeviceId[] = "testDeviceId";
const char16_t kDeviceName[] = u"foobar";
#if defined(FILE_PATH_USES_DRIVE_LETTERS)
base::FilePath::CharType kDevicePath[] = FILE_PATH_LITERAL("C:\\qux");
#else
base::FilePath::CharType kDevicePath[] = FILE_PATH_LITERAL("/qux");
#endif

}  // namespace

class MediaGalleriesPlatformAppBrowserTest : public PlatformAppBrowserTest {
 protected:
  MediaGalleriesPlatformAppBrowserTest() : test_jpg_size_(0) {}
  ~MediaGalleriesPlatformAppBrowserTest() override = default;

  void SetUpOnMainThread() override {
    PlatformAppBrowserTest::SetUpOnMainThread();
    // Prevent the ProcessManager from suspending the chrome-test app. Needed
    // because the writer.onerror and writer.onwriteend events do not qualify as
    // pending callbacks, so the app looks dormant.
    extensions::ProcessManager::SetEventPageIdleTimeForTesting(
        TestTimeouts::action_max_timeout().InMilliseconds());

    std::optional<int64_t> file_size =
        base::GetFileSize(GetCommonDataDir().AppendASCII("test.jpg"));
    ASSERT_TRUE(file_size.has_value());
    test_jpg_size_ = base::checked_cast<int>(file_size.value());
  }

  bool RunMediaGalleriesTest(const std::string& extension_name) {
    base::ListValue empty_list_value;
    return RunMediaGalleriesTestWithArg(extension_name, empty_list_value);
  }

  bool RunMediaGalleriesTestWithArg(const std::string& extension_name,
                                    const base::ListValue& custom_arg_value) {
    // Copy the test data for this test into a temporary directory. Then add
    // a common_injected.js to the temporary copy and run it.
    const char kTestDir[] = "api_test/media_galleries/";
    base::FilePath from_dir =
        test_data_dir_.AppendASCII(kTestDir + extension_name);
    from_dir = from_dir.NormalizePathSeparators();

    base::ScopedAllowBlockingForTesting allow_blocking;
    base::ScopedTempDir temp_dir;
    if (!temp_dir.CreateUniqueTempDir())
      return false;

    if (!base::CopyDirectory(from_dir, temp_dir.GetPath(), true))
      return false;

    base::FilePath common_js_path(
        GetCommonDataDir().AppendASCII("common_injected.js"));
    base::FilePath inject_js_path(temp_dir.GetPath()
                                      .AppendASCII(extension_name)
                                      .AppendASCII("common_injected.js"));
    if (!base::CopyFile(common_js_path, inject_js_path))
      return false;

    const char* custom_arg = nullptr;
    std::string json_string;
    if (!custom_arg_value.empty()) {
      json_string = base::WriteJson(custom_arg_value).value_or("");
      custom_arg = json_string.c_str();
    }

    base::AutoReset<base::FilePath> reset(&test_data_dir_, temp_dir.GetPath());
    bool result = RunExtensionTest(
        extension_name.c_str(),
        {.custom_arg = custom_arg, .launch_as_platform_app = true});
    content::RunAllPendingInMessageLoop();  // avoid race on exit in registry.
    return result;
  }

  void AttachFakeDevice() {
    device_id_ = StorageInfo::MakeDeviceId(
        StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM, kDeviceId);

    StorageMonitor::GetInstance()->receiver()->ProcessAttach(
        StorageInfo(device_id_, kDevicePath, kDeviceName, std::u16string(),
                    std::u16string(), 0));
    content::RunAllPendingInMessageLoop();
  }

  void DetachFakeDevice() {
    StorageMonitor::GetInstance()->receiver()->ProcessDetach(device_id_);
    content::RunAllPendingInMessageLoop();
  }

  // Called if test only wants a single gallery it creates.
  void RemoveAllGalleries() {
    MediaGalleriesPreferences* preferences = GetAndInitializePreferences();

    // Make a copy, as the iterator would be invalidated otherwise.
    const MediaGalleriesPrefInfoMap galleries = preferences->known_galleries();
    for (auto it = galleries.begin(); it != galleries.end(); ++it) {
      preferences->ForgetGalleryById(it->first);
    }
  }

  // This function makes a single fake gallery. This is needed to test platforms
  // with no default media galleries, such as CHROMEOS. This fake gallery is
  // pre-populated with a test.jpg and test.txt.
  void MakeSingleFakeGallery(MediaGalleryPrefId* pref_id) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    ASSERT_FALSE(fake_gallery_temp_dir_.IsValid());
    ASSERT_TRUE(fake_gallery_temp_dir_.CreateUniqueTempDir());

    MediaGalleriesPreferences* preferences = GetAndInitializePreferences();

    MediaGalleryPrefInfo gallery_info;
    ASSERT_FALSE(preferences->LookUpGalleryByPath(
        fake_gallery_temp_dir_.GetPath(), &gallery_info));
    MediaGalleryPrefId id = preferences->AddGallery(
        gallery_info.device_id, gallery_info.path,
        MediaGalleryPrefInfo::kAutoDetected, gallery_info.volume_label,
        gallery_info.vendor_name, gallery_info.model_name,
        gallery_info.total_size_in_bytes, gallery_info.last_attach_time, 0, 0,
        0);
    if (pref_id)
      *pref_id = id;

    content::RunAllPendingInMessageLoop();

    // Valid file, should show up in JS as a FileEntry.
    AddFileToSingleFakeGallery(GetCommonDataDir().AppendASCII("test.jpg"));

    // Invalid file, should not show up as a FileEntry in JS at all.
    AddFileToSingleFakeGallery(GetCommonDataDir().AppendASCII("test.txt"));
  }

  void AddFileToSingleFakeGallery(const base::FilePath& source_path) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    ASSERT_TRUE(fake_gallery_temp_dir_.IsValid());

    ASSERT_TRUE(base::CopyFile(
        source_path,
        fake_gallery_temp_dir_.GetPath().Append(source_path.BaseName())));
  }

  base::FilePath GetCommonDataDir() const {
    return test_data_dir_.AppendASCII("api_test")
        .AppendASCII("media_galleries")
        .AppendASCII("common");
  }

  int test_jpg_size() const { return test_jpg_size_; }

 private:
  MediaGalleriesPreferences* GetAndInitializePreferences() {
    MediaGalleriesPreferences* preferences =
        g_browser_process->media_file_system_registry()->GetPreferences(
            browser()->GetProfile());
    base::RunLoop runloop;
    preferences->EnsureInitialized(runloop.QuitClosure());
    runloop.Run();
    return preferences;
  }

  std::string device_id_;
  base::ScopedTempDir fake_gallery_temp_dir_;
  int test_jpg_size_;
};

// Test is flaky. See crbug.com/41095977.
IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest,
                       DISABLED_MediaGalleriesNoAccess) {
  MakeSingleFakeGallery(nullptr);

  base::ListValue custom_args;
  custom_args.Append(1);

  ASSERT_TRUE(RunMediaGalleriesTestWithArg("no_access", custom_args))
      << message_;
}

IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest, NoGalleriesRead) {
  ASSERT_TRUE(RunMediaGalleriesTest("no_galleries")) << message_;
}

IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest,
                       NoGalleriesCopyTo) {
  ASSERT_TRUE(RunMediaGalleriesTest("no_galleries_copy_to")) << message_;
}

IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest,
                       MediaGalleriesRead) {
  RemoveAllGalleries();
  MakeSingleFakeGallery(nullptr);
  base::ListValue custom_args;
  custom_args.Append(test_jpg_size());

  ASSERT_TRUE(RunMediaGalleriesTestWithArg("read_access", custom_args))
      << message_;
}

// Test is flaky. See crbug.com/41095977.
IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest,
                       DISABLED_MediaGalleriesCopyTo) {
  RemoveAllGalleries();
  MakeSingleFakeGallery(nullptr);
  ASSERT_TRUE(RunMediaGalleriesTest("copy_to_access")) << message_;
}

IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest,
                       MediaGalleriesDelete) {
  MakeSingleFakeGallery(nullptr);
  base::ListValue custom_args;
  custom_args.Append(1);
  ASSERT_TRUE(RunMediaGalleriesTestWithArg("delete_access", custom_args))
      << message_;
}

IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest,
                       MediaGalleriesAccessAttached) {
  AttachFakeDevice();

  base::ListValue custom_args;
  custom_args.Append(1);
  custom_args.Append(kDeviceName);

  ASSERT_TRUE(RunMediaGalleriesTestWithArg("access_attached", custom_args))
      << message_;

  DetachFakeDevice();
}

IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest, ToURL) {
  RemoveAllGalleries();
  MediaGalleryPrefId pref_id;
  MakeSingleFakeGallery(&pref_id);

  base::ListValue custom_args;
  custom_args.Append(base::checked_cast<int>(pref_id));
  custom_args.Append(browser()->GetProfile()->GetBaseName().MaybeAsASCII());

  ASSERT_TRUE(RunMediaGalleriesTestWithArg("tourl", custom_args)) << message_;
}

IN_PROC_BROWSER_TEST_F(MediaGalleriesPlatformAppBrowserTest, GetMetadata) {
  RemoveAllGalleries();
  MakeSingleFakeGallery(nullptr);

  AddFileToSingleFakeGallery(media::GetTestDataFilePath("90rotation.mp4"));
  AddFileToSingleFakeGallery(media::GetTestDataFilePath("id3_png_test.mp3"));

  base::ListValue custom_args;
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
  custom_args.Append(true);
#else
  custom_args.Append(false);
#endif
  ASSERT_TRUE(RunMediaGalleriesTestWithArg("media_metadata", custom_args))
      << message_;
}
