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

#include "base/containers/span.h"


#include "chrome/browser/media_galleries/fileapi/native_media_file_util.h"

#include <stddef.h>

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/media_galleries/fileapi/media_file_system_backend.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "storage/browser/blob/shareable_file_reference.h"
#include "storage/browser/file_system/copy_or_move_hook_delegate.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_backend.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_operation.h"
#include "storage/browser/file_system/file_system_operation_runner.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/browser/file_system/isolated_context.h"
#include "storage/browser/file_system/native_file_util.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "storage/browser/test/test_file_system_options.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/gurl.h"
#include "url/origin.h"

#define FPL(x) FILE_PATH_LITERAL(x)

using storage::FileSystemOperation;
using storage::FileSystemURL;

namespace {

typedef FileSystemOperation::FileEntryList FileEntryList;

struct FilteringTestCase {
  const base::FilePath::CharType* path;
  bool is_directory;
  bool visible;
  bool media_file;
  const char* content;
};

const auto kFilteringTestCases = std::to_array<FilteringTestCase>({
    // Directory should always be visible.
    {FPL("hoge"), true, true, false, nullptr},
    {FPL("fuga.jpg"), true, true, false, nullptr},
    {FPL("piyo.txt"), true, true, false, nullptr},
    {FPL("moga.cod"), true, true, false, nullptr},

    // File should be visible if it's a supported media file.
    // File without extension.
    {FPL("foo"), false, false, false, "abc"},
    // Supported media file.
    {FPL("bar.jpg"), false, true, true, "\xFF\xD8\xFF"},
    // Unsupported masquerading file.
    {FPL("sna.jpg"), false, true, false, "abc"},
    // Non-media file.
    {FPL("baz.txt"), false, false, false, "abc"},
    // Unsupported media file.
    {FPL("foobar.cod"), false, false, false, "abc"},
});

void ExpectEqHelper(const std::string& test_name,
                    base::File::Error expected,
                    base::File::Error actual) {
  EXPECT_EQ(expected, actual) << test_name;
}

void ExpectMetadataEqHelper(const std::string& test_name,
                            base::File::Error expected,
                            bool expected_is_directory,
                            base::File::Error actual,
                            const base::File::Info& file_info) {
  EXPECT_EQ(expected, actual) << test_name;
  if (actual == base::File::FILE_OK)
    EXPECT_EQ(expected_is_directory, file_info.is_directory) << test_name;
}

void DidReadDirectory(std::set<base::FilePath::StringType>* content,
                      bool* completed,
                      base::File::Error error,
                      FileEntryList file_list,
                      bool has_more) {
  EXPECT_TRUE(!*completed);
  *completed = !has_more;
  for (const auto& entry : file_list)
    EXPECT_TRUE(content->insert(entry.name.value()).second);
}

void PopulateDirectoryWithTestCases(
    const base::FilePath& dir,
    base::span<const FilteringTestCase> test_cases,
    size_t n) {
  for (size_t i = 0; i < n; ++i) {
    base::FilePath path = dir.Append(test_cases[i].path);
    if (test_cases[i].is_directory) {
      ASSERT_TRUE(base::CreateDirectory(path));
    } else {
      ASSERT_TRUE(test_cases[i].content);
      ASSERT_TRUE(base::WriteFile(path, test_cases[i].content));
    }
  }
}

}  // namespace

class NativeMediaFileUtilTest : public testing::Test {
 public:
  NativeMediaFileUtilTest() = default;

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

  void SetUp() override {
    ASSERT_TRUE(data_dir_.CreateUniqueTempDir());
    ASSERT_TRUE(base::CreateDirectory(root_path()));

    auto storage_policy =
        base::MakeRefCounted<storage::MockSpecialStoragePolicy>();

    std::vector<std::unique_ptr<storage::FileSystemBackend>>
        additional_providers;
    additional_providers.push_back(
        std::make_unique<MediaFileSystemBackend>(data_dir_.GetPath()));

    file_system_context_ = storage::FileSystemContext::Create(
        content::GetIOThreadTaskRunner({}),
        base::SequencedTaskRunner::GetCurrentDefault(),
        storage::ExternalMountPoints::CreateRefCounted(),
        std::move(storage_policy),
        /* quota_manager_proxy=*/nullptr, std::move(additional_providers),
        std::vector<storage::URLRequestAutoMountHandler>(), data_dir_.GetPath(),
        storage::CreateAllowFileAccessOptions());

    filesystem_ = isolated_context()->RegisterFileSystemForPath(
        storage::kFileSystemTypeLocalMedia, std::string(), root_path(),
        nullptr);
    filesystem_id_ = filesystem_.id();
  }

  void TearDown() override { file_system_context_.reset(); }

 protected:
  storage::FileSystemContext* file_system_context() {
    return file_system_context_.get();
  }

  FileSystemURL CreateURL(const base::FilePath::CharType* test_case_path) {
    return file_system_context_->CreateCrackedFileSystemURL(
        blink::StorageKey::CreateFirstParty(url::Origin::Create(origin())),
        storage::kFileSystemTypeIsolated, GetVirtualPath(test_case_path));
  }

  storage::IsolatedContext* isolated_context() {
    return storage::IsolatedContext::GetInstance();
  }

  base::FilePath root_path() {
    return data_dir_.GetPath().Append(FPL("Media Directory"));
  }

  base::FilePath GetVirtualPath(
      const base::FilePath::CharType* test_case_path) {
    return base::FilePath::FromUTF8Unsafe(filesystem_id_)
        .Append(FPL("Media Directory"))
        .Append(base::FilePath(test_case_path));
  }

  GURL origin() { return GURL("http://example.com"); }

  storage::FileSystemType type() { return storage::kFileSystemTypeLocalMedia; }

  storage::FileSystemOperationRunner* operation_runner() {
    return file_system_context_->operation_runner();
  }

  content::BrowserTaskEnvironment task_environment_;

 private:
  base::ScopedTempDir data_dir_;
  scoped_refptr<storage::FileSystemContext> file_system_context_;

  std::string filesystem_id_;
  storage::IsolatedContext::ScopedFSHandle filesystem_;
};

TEST_F(NativeMediaFileUtilTest, DirectoryExistsAndFileExistsFiltering) {
  PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                 std::size(kFilteringTestCases));

  for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
    FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

    base::File::Error expectation = kFilteringTestCases[i].visible
                                        ? base::File::FILE_OK
                                        : base::File::FILE_ERROR_NOT_FOUND;

    std::string test_name =
        base::StringPrintf("DirectoryExistsAndFileExistsFiltering %" PRIuS, i);
    if (kFilteringTestCases[i].is_directory) {
      operation_runner()->DirectoryExists(
          url, base::BindOnce(&ExpectEqHelper, test_name, expectation));
    } else {
      operation_runner()->FileExists(
          url, base::BindOnce(&ExpectEqHelper, test_name, expectation));
    }
    content::RunAllTasksUntilIdle();
  }
}

TEST_F(NativeMediaFileUtilTest, ReadDirectoryFiltering) {
  PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                 std::size(kFilteringTestCases));

  std::set<base::FilePath::StringType> content;
  FileSystemURL url = CreateURL(FPL(""));
  bool completed = false;
  operation_runner()->ReadDirectory(
      url, base::BindRepeating(&DidReadDirectory, &content, &completed));
  content::RunAllTasksUntilIdle();
  EXPECT_TRUE(completed);
  EXPECT_EQ(6u, content.size());

  for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
    base::FilePath::StringType name =
        base::FilePath(kFilteringTestCases[i].path).BaseName().value();
    auto found = content.find(name);
    EXPECT_EQ(kFilteringTestCases[i].visible, found != content.end());
  }
}

TEST_F(NativeMediaFileUtilTest, CreateDirectoryFiltering) {
  // Run the loop twice. The second loop attempts to create directories that are
  // pre-existing. Though the result should be the same.
  for (int loop_count = 0; loop_count < 2; ++loop_count) {
    for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
      if (kFilteringTestCases[i].is_directory) {
        FileSystemURL root_url = CreateURL(FPL(""));
        FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

        std::string test_name = base::StringPrintf(
            "CreateFileAndCreateDirectoryFiltering run %d, test %" PRIuS,
            loop_count, i);
        base::File::Error expectation = kFilteringTestCases[i].visible
                                            ? base::File::FILE_OK
                                            : base::File::FILE_ERROR_SECURITY;
        operation_runner()->CreateDirectory(
            url, false, false,
            base::BindOnce(&ExpectEqHelper, test_name, expectation));
      }
      content::RunAllTasksUntilIdle();
    }
  }
}

TEST_F(NativeMediaFileUtilTest, CopySourceFiltering) {
  base::FilePath dest_path = root_path().AppendASCII("dest");
  FileSystemURL dest_url = CreateURL(FPL("dest"));

  // Run the loop twice. The first run has no source files. The second run does.
  for (int loop_count = 0; loop_count < 2; ++loop_count) {
    if (loop_count == 1) {
      PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                     std::size(kFilteringTestCases));
    }
    for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
      // Always start with an empty destination directory.
      // Copying to a non-empty destination directory is an invalid operation.
      ASSERT_TRUE(base::DeletePathRecursively(dest_path));
      ASSERT_TRUE(base::CreateDirectory(dest_path));

      FileSystemURL root_url = CreateURL(FPL(""));
      FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

      std::string test_name = base::StringPrintf(
          "CopySourceFiltering run %d test %" PRIuS, loop_count, i);
      base::File::Error expectation = base::File::FILE_OK;
      if (loop_count == 0 || !kFilteringTestCases[i].visible) {
        // If the source does not exist or is not visible.
        expectation = base::File::FILE_ERROR_NOT_FOUND;
      } else if (!kFilteringTestCases[i].is_directory) {
        // Cannot copy a visible file to a directory.
        expectation = base::File::FILE_ERROR_INVALID_OPERATION;
      }
      operation_runner()->Copy(
          url, dest_url, storage::FileSystemOperation::CopyOrMoveOptionSet(),
          storage::FileSystemOperation::ERROR_BEHAVIOR_ABORT,
          std::make_unique<storage::CopyOrMoveHookDelegate>(),
          base::BindOnce(&ExpectEqHelper, test_name, expectation));
      content::RunAllTasksUntilIdle();
    }
  }
}

TEST_F(NativeMediaFileUtilTest, CopyDestFiltering) {
  // Run the loop twice. The first run has no destination files.
  // The second run does.
  for (int loop_count = 0; loop_count < 2; ++loop_count) {
    if (loop_count == 1) {
      // Reset the test directory between the two loops to remove old
      // directories and create new ones that should pre-exist.
      ASSERT_TRUE(base::DeletePathRecursively(root_path()));
      ASSERT_TRUE(base::CreateDirectory(root_path()));
      PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                     std::size(kFilteringTestCases));
    }

    // Always create a dummy source data file.
    base::FilePath src_path = root_path().AppendASCII("foo.jpg");
    FileSystemURL src_url = CreateURL(FPL("foo.jpg"));
    static const char kDummyData[] = "dummy";
    ASSERT_TRUE(base::WriteFile(src_path, kDummyData));

    for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
      if (loop_count == 0 && kFilteringTestCases[i].is_directory) {
        // These directories do not exist in this case, so Copy() will not
        // treat them as directories. Thus invalidating these test cases.
        continue;
      }
      FileSystemURL root_url = CreateURL(FPL(""));
      FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

      std::string test_name = base::StringPrintf(
          "CopyDestFiltering run %d test %" PRIuS, loop_count, i);
      base::File::Error expectation;
      if (loop_count == 0) {
        // The destination path is a file here. The directory case has been
        // handled above.
        // If the destination path does not exist and is not visible, then
        // creating it would be a security violation.
        expectation = kFilteringTestCases[i].visible
                          ? base::File::FILE_OK
                          : base::File::FILE_ERROR_SECURITY;
      } else {
        if (!kFilteringTestCases[i].visible) {
          // If the destination path exist and is not visible, then to the copy
          // operation, it looks like the file needs to be created, which is a
          // security violation.
          expectation = base::File::FILE_ERROR_SECURITY;
        } else if (kFilteringTestCases[i].is_directory) {
          // Cannot copy a file to a directory.
          expectation = base::File::FILE_ERROR_INVALID_OPERATION;
        } else {
          // Copying from a file to a visible file that exists is ok.
          expectation = base::File::FILE_OK;
        }
      }
      operation_runner()->Copy(
          src_url, url, storage::FileSystemOperation::CopyOrMoveOptionSet(),
          storage::FileSystemOperation::ERROR_BEHAVIOR_ABORT,
          std::make_unique<storage::CopyOrMoveHookDelegate>(),
          base::BindOnce(&ExpectEqHelper, test_name, expectation));
      content::RunAllTasksUntilIdle();
    }
  }
}

TEST_F(NativeMediaFileUtilTest, MoveSourceFiltering) {
  base::FilePath dest_path = root_path().AppendASCII("dest");
  FileSystemURL dest_url = CreateURL(FPL("dest"));

  // Run the loop twice. The first run has no source files. The second run does.
  for (int loop_count = 0; loop_count < 2; ++loop_count) {
    if (loop_count == 1) {
      PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                     std::size(kFilteringTestCases));
    }
    for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
      // Always start with an empty destination directory.
      // Moving to a non-empty destination directory is an invalid operation.
      ASSERT_TRUE(base::DeletePathRecursively(dest_path));
      ASSERT_TRUE(base::CreateDirectory(dest_path));

      FileSystemURL root_url = CreateURL(FPL(""));
      FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

      std::string test_name = base::StringPrintf(
          "MoveSourceFiltering run %d test %" PRIuS, loop_count, i);
      base::File::Error expectation = base::File::FILE_OK;
      if (loop_count == 0 || !kFilteringTestCases[i].visible) {
        // If the source does not exist or is not visible.
        expectation = base::File::FILE_ERROR_NOT_FOUND;
      } else if (!kFilteringTestCases[i].is_directory) {
        // Cannot move a visible file to a directory.
        expectation = base::File::FILE_ERROR_INVALID_OPERATION;
      }
      operation_runner()->Move(
          url, dest_url, storage::FileSystemOperation::CopyOrMoveOptionSet(),
          storage::FileSystemOperation::ERROR_BEHAVIOR_ABORT,
          std::make_unique<storage::CopyOrMoveHookDelegate>(),
          base::BindOnce(&ExpectEqHelper, test_name, expectation));
      content::RunAllTasksUntilIdle();
    }
  }
}

TEST_F(NativeMediaFileUtilTest, MoveDestFiltering) {
  // Run the loop twice. The first run has no destination files.
  // The second run does.
  for (int loop_count = 0; loop_count < 2; ++loop_count) {
    if (loop_count == 1) {
      // Reset the test directory between the two loops to remove old
      // directories and create new ones that should pre-exist.
      ASSERT_TRUE(base::DeletePathRecursively(root_path()));
      ASSERT_TRUE(base::CreateDirectory(root_path()));
      PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                     std::size(kFilteringTestCases));
    }

    for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
      if (loop_count == 0 && kFilteringTestCases[i].is_directory) {
        // These directories do not exist in this case, so Copy() will not
        // treat them as directories. Thus invalidating these test cases.
        continue;
      }

      // Create the source file for every test case because it might get moved.
      base::FilePath src_path = root_path().AppendASCII("foo.jpg");
      FileSystemURL src_url = CreateURL(FPL("foo.jpg"));
      static const char kDummyData[] = "dummy";
      ASSERT_TRUE(base::WriteFile(src_path, kDummyData));

      FileSystemURL root_url = CreateURL(FPL(""));
      FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

      std::string test_name = base::StringPrintf(
          "MoveDestFiltering run %d test %" PRIuS, loop_count, i);
      base::File::Error expectation;
      if (loop_count == 0) {
        // The destination path is a file here. The directory case has been
        // handled above.
        // If the destination path does not exist and is not visible, then
        // creating it would be a security violation.
        expectation = kFilteringTestCases[i].visible
                          ? base::File::FILE_OK
                          : base::File::FILE_ERROR_SECURITY;
      } else {
        if (!kFilteringTestCases[i].visible) {
          // If the destination path exist and is not visible, then to the move
          // operation, it looks like the file needs to be created, which is a
          // security violation.
          expectation = base::File::FILE_ERROR_SECURITY;
        } else if (kFilteringTestCases[i].is_directory) {
          // Cannot move a file to a directory.
          expectation = base::File::FILE_ERROR_INVALID_OPERATION;
        } else {
          // Moving from a file to a visible file that exists is ok.
          expectation = base::File::FILE_OK;
        }
      }
      operation_runner()->Move(
          src_url, url, storage::FileSystemOperation::CopyOrMoveOptionSet(),
          storage::FileSystemOperation::ERROR_BEHAVIOR_ABORT,
          std::make_unique<storage::CopyOrMoveHookDelegate>(),
          base::BindOnce(&ExpectEqHelper, test_name, expectation));
      content::RunAllTasksUntilIdle();
    }
  }
}

TEST_F(NativeMediaFileUtilTest, GetMetadataFiltering) {
  // Run the loop twice. The first run has no files. The second run does.
  for (int loop_count = 0; loop_count < 2; ++loop_count) {
    if (loop_count == 1) {
      PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                     std::size(kFilteringTestCases));
    }
    for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
      FileSystemURL root_url = CreateURL(FPL(""));
      FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

      std::string test_name = base::StringPrintf(
          "GetMetadataFiltering run %d test %" PRIuS, loop_count, i);
      base::File::Error expectation = base::File::FILE_OK;
      if (loop_count == 0 || !kFilteringTestCases[i].visible) {
        // Cannot get metadata from files that do not exist or are not visible.
        expectation = base::File::FILE_ERROR_NOT_FOUND;
      }
      operation_runner()->GetMetadata(
          url, {storage::FileSystemOperation::GetMetadataField::kIsDirectory},
          base::BindOnce(&ExpectMetadataEqHelper, test_name, expectation,
                         kFilteringTestCases[i].is_directory));
      content::RunAllTasksUntilIdle();
    }
  }
}

TEST_F(NativeMediaFileUtilTest, RemoveFileFiltering) {
  // Run the loop twice. The first run has no files. The second run does.
  for (int loop_count = 0; loop_count < 2; ++loop_count) {
    if (loop_count == 1) {
      PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                     std::size(kFilteringTestCases));
    }
    for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
      FileSystemURL root_url = CreateURL(FPL(""));
      FileSystemURL url = CreateURL(kFilteringTestCases[i].path);

      std::string test_name = base::StringPrintf(
          "RemoveFiltering run %d test %" PRIuS, loop_count, i);
      base::File::Error expectation = base::File::FILE_OK;
      if (loop_count == 0 || !kFilteringTestCases[i].visible) {
        // Cannot remove files that do not exist or are not visible.
        expectation = base::File::FILE_ERROR_NOT_FOUND;
      } else if (kFilteringTestCases[i].is_directory) {
        expectation = base::File::FILE_ERROR_NOT_A_FILE;
      }
      operation_runner()->RemoveFile(
          url, base::BindOnce(&ExpectEqHelper, test_name, expectation));
      content::RunAllTasksUntilIdle();
    }
  }
}

void CreateSnapshotCallback(base::File::Error* error,
                            base::File::Error result,
                            const base::File::Info&,
                            const base::FilePath&,
                            scoped_refptr<storage::ShareableFileReference>) {
  *error = result;
}

TEST_F(NativeMediaFileUtilTest, CreateSnapshot) {
  PopulateDirectoryWithTestCases(root_path(), kFilteringTestCases,
                                 std::size(kFilteringTestCases));
  for (size_t i = 0; i < std::size(kFilteringTestCases); ++i) {
    if (kFilteringTestCases[i].is_directory ||
        !kFilteringTestCases[i].visible) {
      continue;
    }
    FileSystemURL root_url = CreateURL(FPL(""));
    FileSystemURL url = CreateURL(kFilteringTestCases[i].path);
    base::File::Error expected_error, error;
    if (kFilteringTestCases[i].media_file)
      expected_error = base::File::FILE_OK;
    else
      expected_error = base::File::FILE_ERROR_SECURITY;
    error = base::File::FILE_ERROR_FAILED;
    operation_runner()->CreateSnapshotFile(
        url, base::BindOnce(CreateSnapshotCallback, &error));
    content::RunAllTasksUntilIdle();
    ASSERT_EQ(expected_error, error);
  }
}
