// 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 "components/update_client/crx_cache.h"

#include <map>
#include <memory>
#include <string>
#include <utility>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/types/expected.h"
#include "components/update_client/test_utils.h"
#include "components/update_client/update_client_errors.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace update_client {
namespace {

auto ExpectPathExists() {
  return base::BindOnce(
      [](base::expected<base::FilePath, UnpackerError> result) {
        ASSERT_TRUE(result.has_value()) << std::to_underlying(result.error());
        EXPECT_TRUE(base::PathExists(result.value())) << result.value().value();
      });
}

auto ExpectError(UnpackerError error) {
  return base::BindOnce(
      [](UnpackerError error,
         base::expected<base::FilePath, UnpackerError> result) {
        ASSERT_FALSE(result.has_value()) << result.value();
        EXPECT_EQ(result.error(), error);
      },
      error);
}

auto ExpectHashes(
    const std::multimap<std::string, std::string>& expected_hashes) {
  return base::BindOnce(
      [](const std::multimap<std::string, std::string>& expected_hashes,
         const std::multimap<std::string, std::string>& result) {
        EXPECT_EQ(result, expected_hashes);
      },
      expected_hashes);
}

}  // namespace

class CrxCacheTest : public testing::Test {
 protected:
  void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }

  base::FilePath TempPath(const std::string& basename) {
    return temp_dir_.GetPath().AppendUTF8(basename);
  }

  base::FilePath MakeFile() {
    base::FilePath out;
    EXPECT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.GetPath(), &out));
    return out;
  }

  void RunLoop() {
    loop_->Run();
    loop_ = std::make_unique<base::RunLoop>();
  }

  base::RepeatingClosure Quit() { return loop_->QuitClosure(); }

 private:
  base::ScopedTempDir temp_dir_;
  base::test::TaskEnvironment env_;
  std::unique_ptr<base::RunLoop> loop_ = std::make_unique<base::RunLoop>();
};

TEST_F(CrxCacheTest, PutGet) {
  scoped_refptr<CrxCache> cache =
      base::MakeRefCounted<CrxCache>(TempPath("cache_dir"));
  cache->Put(MakeFile(), "appid", "hash", ExpectPathExists());
  cache->GetByHash("hash", ExpectPathExists().Then(Quit()));
  RunLoop();
}

TEST_F(CrxCacheTest, GetMissing) {
  scoped_refptr<CrxCache> cache =
      base::MakeRefCounted<CrxCache>(TempPath("cache_dir"));
  cache->Put(MakeFile(), "appid", "hash", ExpectPathExists());
  cache->GetByHash(
      "hash2", ExpectError(UnpackerError::kCrxCacheFileNotCached).Then(Quit()));
  RunLoop();
}

TEST_F(CrxCacheTest, PutReplacesByAppId) {
  scoped_refptr<CrxCache> cache =
      base::MakeRefCounted<CrxCache>(TempPath("cache_dir"));
  cache->Put(MakeFile(), "appid", "hash", ExpectPathExists());
  cache->GetByHash("hash", ExpectPathExists().Then(Quit()));
  RunLoop();
  cache->Put(MakeFile(), "appid", "hash2", ExpectPathExists());
  cache->GetByHash("hash", ExpectError(UnpackerError::kCrxCacheFileNotCached));
  cache->GetByHash("hash2", ExpectPathExists().Then(Quit()));
  RunLoop();
}

TEST_F(CrxCacheTest, PutAlreadyCached) {
  scoped_refptr<CrxCache> cache =
      base::MakeRefCounted<CrxCache>(TempPath("cache_dir"));
  cache->Put(MakeFile(), "appid", "hash", ExpectPathExists());
  cache->GetByHash(
      "hash",
      base::BindLambdaForTesting(
          [&](base::expected<base::FilePath, UnpackerError> result) {
            if (!result.has_value()) {
              Quit().Run();
            }
            ASSERT_TRUE(result.has_value())
                << std::to_underlying(result.error());
            cache->Put(
                result.value(), "appid", "hash",
                base::BindLambdaForTesting(
                    [&](base::expected<base::FilePath, UnpackerError> result2) {
                      if (!result2.has_value()) {
                        Quit().Run();
                      }
                      ASSERT_TRUE(result2.has_value())
                          << std::to_underlying(result2.error());
                      cache->GetByHash("hash", ExpectPathExists().Then(Quit()));
                    }));
          }));
  RunLoop();
}

TEST_F(CrxCacheTest, CacheNotProvided) {
  scoped_refptr<CrxCache> cache = base::MakeRefCounted<CrxCache>(std::nullopt);
  cache->Put(base::FilePath(FILE_PATH_LITERAL("crxcache_test_file")), "appid",
             "hash", ExpectError(UnpackerError::kCrxCacheNotProvided));
  cache->GetByHash("hash", ExpectError(UnpackerError::kCrxCacheNotProvided));
  cache->RemoveAll("appid", base::DoNothing());
  cache->ListHashesByAppId(ExpectHashes({}).Then(Quit()));
  RunLoop();
}

TEST_F(CrxCacheTest, ListHashesByAppId) {
  scoped_refptr<CrxCache> cache =
      base::MakeRefCounted<CrxCache>(TempPath("cache_dir"));
  cache->ListHashesByAppId(ExpectHashes({}));
  cache->Put(MakeFile(), "appid", "hash", ExpectPathExists());
  cache->ListHashesByAppId(ExpectHashes({{"appid", "hash"}}));
  cache->Put(MakeFile(), "appid2", "hash2", ExpectPathExists());
  cache->ListHashesByAppId(
      ExpectHashes({{"appid", "hash"}, {"appid2", "hash2"}}).Then(Quit()));
  RunLoop();
  cache->Put(MakeFile(), "appid", "hash3", ExpectPathExists());
  cache->ListHashesByAppId(
      ExpectHashes({{"appid", "hash3"}, {"appid2", "hash2"}}).Then(Quit()));
  RunLoop();
}

TEST_F(CrxCacheTest, RemoveAll) {
  scoped_refptr<CrxCache> cache =
      base::MakeRefCounted<CrxCache>(TempPath("cache_dir"));
  cache->Put(MakeFile(), "appid", "hash", ExpectPathExists());
  cache->Put(MakeFile(), "appid2", "hash2", ExpectPathExists());
  cache->ListHashesByAppId(
      ExpectHashes({{"appid", "hash"}, {"appid2", "hash2"}}).Then(Quit()));
  RunLoop();
  cache->RemoveAll("appid", base::DoNothing());
  cache->ListHashesByAppId(ExpectHashes({{"appid2", "hash2"}}).Then(Quit()));
  RunLoop();
  cache->RemoveAll("appid2", base::DoNothing());
  cache->ListHashesByAppId(ExpectHashes({}).Then(Quit()));
  RunLoop();
}

}  // namespace update_client
