/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <array>

#include "FileInfo.h"
#include "FileInfoImpl.h"
#include "FileInfoManager.h"
#include "gtest/gtest.h"
#include "mozilla/ArrayAlgorithm.h"
#include "mozilla/StaticMutex.h"
#include "nsTArray.h"

using namespace mozilla;
using namespace mozilla::dom::indexedDB;

class SimpleFileManager;

using SimpleFileInfo = FileInfo<SimpleFileManager>;

struct SimpleFileManagerStats final {
  // XXX We don't keep track of the specific aFileId parameters here, should we?

  size_t mAsyncDeleteFileCalls = 0;
  size_t mSyncDeleteFileCalls = 0;
};

class SimpleFileManager final : public FileInfoManager<SimpleFileManager>,
                                public AtomicSafeRefCounted<SimpleFileManager> {
 public:
  using FileInfoManager<SimpleFileManager>::MutexType;

  MOZ_DECLARE_REFCOUNTED_TYPENAME(SimpleFileManager)

  // SimpleFileManager functions that are used by SimpleFileInfo

  [[nodiscard]] nsresult AsyncDeleteFile(const int64_t aFileId) {
    MOZ_RELEASE_ASSERT(!ContainsFileInfo(aFileId));

    if (mStats) {
      ++mStats->mAsyncDeleteFileCalls;
    }

    return NS_OK;
  }

  [[nodiscard]] nsresult SyncDeleteFile(const int64_t aFileId) {
    MOZ_RELEASE_ASSERT(!ContainsFileInfo(aFileId));

    if (mStats) {
      ++mStats->mSyncDeleteFileCalls;
    }
    return NS_OK;
  }

  // Test-specific functions
  explicit SimpleFileManager(SimpleFileManagerStats* aStats = nullptr)
      : mStats{aStats} {}

  void CreateDBOnlyFileInfos() {
    for (const auto id : kDBOnlyFileInfoIds) {
      // Copied from within DatabaseFileManager::Init.

      MOZ_RELEASE_ASSERT(CreateFileInfo(Some(id), static_cast<nsrefcnt>(1)));
    }
  }

  static MutexType& MutexInstance() MOZ_RETURN_CAPABILITY(sMutex) {
    return sMutex;
  }

  static constexpr auto kDBOnlyFileInfoIds =
      std::array<int64_t, 3>{{10, 20, 30}};

 private:
  inline static MutexType sMutex;

  SimpleFileManagerStats* const mStats;
};

// These tests test the SimpleFileManager itself, to ensure the SimpleFileInfo
// tests below are valid.

TEST(DOM_IndexedDB_SimpleFileManager, Invalidate)
{
  const auto fileManager = MakeSafeRefPtr<SimpleFileManager>();

  fileManager->Invalidate();

  ASSERT_TRUE(fileManager->Invalidated());
}

// These tests mainly test SimpleFileInfo, which is a simplified version of
// DatabaseFileInfo (SimpleFileInfo doesn't work with real files stored on
// disk). The actual objects, DatabaseFileInfo and DatabaseFileManager are not
// tested here.

TEST(DOM_IndexedDB_SimpleFileInfo, Create)
{
  auto stats = SimpleFileManagerStats{};

  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);
    auto fileInfo = fileManager->CreateFileInfo();

    int32_t memRefCnt, dbRefCnt;
    fileInfo->GetReferences(&memRefCnt, &dbRefCnt);

    ASSERT_EQ(fileManager, &fileInfo->Manager());

    ASSERT_EQ(1, memRefCnt);
    ASSERT_EQ(0, dbRefCnt);
  }

  ASSERT_EQ(0u, stats.mSyncDeleteFileCalls);
  ASSERT_EQ(1u, stats.mAsyncDeleteFileCalls);
}

TEST(DOM_IndexedDB_SimpleFileInfo, CreateWithInitialDBRefCnt)
{
  auto stats = SimpleFileManagerStats{};

  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);
    fileManager->CreateDBOnlyFileInfos();

    for (const auto id : SimpleFileManager::kDBOnlyFileInfoIds) {
      const auto fileInfo = fileManager->GetFileInfo(id);
      ASSERT_NE(nullptr, fileInfo);

      int32_t memRefCnt, dbRefCnt;
      fileInfo->GetReferences(&memRefCnt, &dbRefCnt);

      ASSERT_EQ(fileManager, &fileInfo->Manager());

      ASSERT_EQ(1, memRefCnt);  // we hold one in fileInfo ourselves
      ASSERT_EQ(1, dbRefCnt);
    }
  }

  ASSERT_EQ(0u, stats.mSyncDeleteFileCalls);
  // Since the files have still non-zero dbRefCnt, nothing must be deleted.
  ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls);
}

TEST(DOM_IndexedDB_SimpleFileInfo, CreateWithInitialDBRefCnt_Invalidate)
{
  auto stats = SimpleFileManagerStats{};

  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);
    fileManager->CreateDBOnlyFileInfos();

    const auto fileInfos = TransformIntoNewArray(
        SimpleFileManager::kDBOnlyFileInfoIds,
        [&fileManager](const auto id) { return fileManager->GetFileInfo(id); });

    fileManager->Invalidate();

    for (const auto& fileInfo : fileInfos) {
      int32_t memRefCnt, dbRefCnt;
      fileInfo->GetReferences(&memRefCnt, &dbRefCnt);

      ASSERT_EQ(1, memRefCnt);  // we hold one in fileInfo ourselves
      ASSERT_EQ(0, dbRefCnt);   // dbRefCnt was cleared by Invalidate
    }
  }

  ASSERT_EQ(0u, stats.mSyncDeleteFileCalls);
  // Since the files have still non-zero dbRefCnt, nothing must be deleted.
  ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls);
}

TEST(DOM_IndexedDB_SimpleFileInfo, CreateWithInitialDBRefCnt_UpdateDBRefsToZero)
{
  auto stats = SimpleFileManagerStats{};

  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);
    fileManager->CreateDBOnlyFileInfos();

    const auto fileInfo =
        fileManager->GetFileInfo(SimpleFileManager::kDBOnlyFileInfoIds[0]);
    fileInfo->UpdateDBRefs(-1);

    int32_t memRefCnt, dbRefCnt;
    fileInfo->GetReferences(&memRefCnt, &dbRefCnt);

    ASSERT_EQ(1, memRefCnt);  // we hold one in fileInfo ourselves
    ASSERT_EQ(0, dbRefCnt);
  }

  ASSERT_EQ(0u, stats.mSyncDeleteFileCalls);
  ASSERT_EQ(1u, stats.mAsyncDeleteFileCalls);
}

TEST(DOM_IndexedDB_SimpleFileInfo, ReleaseWithFileManagerCleanup)
{
  auto stats = SimpleFileManagerStats{};
  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);
    fileManager->CreateDBOnlyFileInfos();

    auto* fileInfo = fileManager->CreateFileInfo().forget().take();
    fileInfo->Release(/* aSyncDeleteFile */ true);

    // This was the only reference and SimpleFileManager was not invalidated,
    // so SimpleFileManager::Cleanup should have been called.
    ASSERT_EQ(1u, stats.mSyncDeleteFileCalls);
  }
  ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls);
}

#ifndef DEBUG
// These tests cause assertion failures in DEBUG builds.

TEST(DOM_IndexedDB_SimpleFileInfo, Invalidate_CreateFileInfo)
{
  auto stats = SimpleFileManagerStats{};
  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);

    fileManager->Invalidate();

    const auto fileInfo = fileManager->CreateFileInfo();
    (void)fileInfo;

    ASSERT_EQ(nullptr, fileInfo);
  }

  ASSERT_EQ(0u, stats.mSyncDeleteFileCalls);
  ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls);
}
#endif

TEST(DOM_IndexedDB_SimpleFileInfo, Invalidate_Release)
{
  auto stats = SimpleFileManagerStats{};
  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);

    const auto fileInfo = fileManager->CreateFileInfo();
    (void)fileInfo;

    fileManager->Invalidate();

    // SimpleFileManager was invalidated, so Release does not do any cleanup.
  }

  ASSERT_EQ(0u, stats.mSyncDeleteFileCalls);
  ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls);
}

TEST(DOM_IndexedDB_SimpleFileInfo, Invalidate_ReleaseWithFileManagerCleanup)
{
  auto stats = SimpleFileManagerStats{};
  {
    const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats);

    auto* fileInfo = fileManager->CreateFileInfo().forget().take();

    fileManager->Invalidate();

    // SimpleFileManager was invalidated, so Release does not do any cleanup.
    fileInfo->Release(/* aSyncDeleteFile */ true);
  }

  ASSERT_EQ(0u, stats.mSyncDeleteFileCalls);
  ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls);
}

// XXX Add test for GetFileForFileInfo
