/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "FileSystemHashSource.h"
#include "FileSystemParentTypes.h"
#include "TestHelpers.h"
#include "gtest/gtest.h"
#include "mozilla/Array.h"
#include "mozilla/dom/FileSystemTypes.h"
#include "nsContentUtils.h"
#include "nsLiteralString.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashSet.h"

namespace mozilla::dom::fs::test {

using mozilla::dom::fs::data::FileSystemHashSource;

namespace {

constexpr size_t sha256ByteLength = 32u;

constexpr size_t kExpectedLength = 52u;

std::wstring asWide(const nsString& aStr) {
  std::wstring result;
  result.reserve(aStr.Length());
  for (const auto* it = aStr.BeginReading(); it != aStr.EndReading(); ++it) {
    result.push_back(static_cast<wchar_t>(*it));
  }
  return result;
}

}  // namespace

TEST(TestFileSystemHashSource, isHashLengthAsExpected)
{
  EntryId parent = "a"_ns;
  Name name = u"b"_ns;
  TEST_TRY_UNWRAP(EntryId result,
                  FileSystemHashSource::GenerateHash(parent, name));
  ASSERT_EQ(sha256ByteLength, result.Length());
};

TEST(TestFileSystemHashSource, areNestedNameHashesValidAndUnequal)
{
  EntryId emptyParent = ""_ns;
  Name name = u"a"_ns;
  const size_t nestingNumber = 500u;

  nsTHashSet<EntryId> results;
  nsTHashSet<Name> names;

  auto previousParent = emptyParent;
  for (size_t i = 0; i < nestingNumber; ++i) {
    TEST_TRY_UNWRAP(EntryId result,
                    FileSystemHashSource::GenerateHash(previousParent, name));

    TEST_TRY_UNWRAP(Name encoded,
                    FileSystemHashSource::EncodeHash(FileId(result)));

    // Validity checks
    ASSERT_TRUE(mozilla::IsAscii(encoded))
    << encoded;
    Name upperCaseVersion;
    nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
    ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());

    // Is the same hash encountered?
    ASSERT_FALSE(results.Contains(result));
    ASSERT_TRUE(results.Insert(result, mozilla::fallible));

    // Is the same name encountered?
    ASSERT_FALSE(names.Contains(encoded));
    ASSERT_TRUE(names.Insert(encoded, mozilla::fallible));

    previousParent = result;
  }
};

TEST(TestFileSystemHashSource, areNameCombinationHashesUnequal)
{
  EntryId emptyParent = ""_ns;

  mozilla::Array<Name, 2> inputs = {u"a"_ns, u"b"_ns};
  nsTArray<EntryId> results;
  nsTArray<Name> names;

  for (const auto& name : inputs) {
    TEST_TRY_UNWRAP(EntryId result,
                    FileSystemHashSource::GenerateHash(emptyParent, name));
    TEST_TRY_UNWRAP(Name encoded,
                    FileSystemHashSource::EncodeHash(FileId(result)));

    // Validity checks
    ASSERT_TRUE(mozilla::IsAscii(encoded))
    << encoded;
    Name upperCaseVersion;
    nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
    ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());

    results.AppendElement(result);
    names.AppendElement(encoded);
  }

  nsTArray<EntryId> more_results;
  nsTArray<Name> more_names;
  for (const auto& parent : results) {
    for (const auto& name : inputs) {
      TEST_TRY_UNWRAP(EntryId result,
                      FileSystemHashSource::GenerateHash(parent, name));
      TEST_TRY_UNWRAP(Name encoded,
                      FileSystemHashSource::EncodeHash(FileId(result)));

      // Validity checks
      ASSERT_TRUE(mozilla::IsAscii(encoded))
      << encoded;
      Name upperCaseVersion;
      nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
      ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());

      more_results.AppendElement(result);
      more_names.AppendElement(encoded);
    }
  }

  results.AppendElements(more_results);
  names.AppendElements(more_names);

  // Is the same hash encountered?
  for (size_t i = 0; i < results.Length(); ++i) {
    for (size_t j = i + 1; j < results.Length(); ++j) {
      ASSERT_STRNE(results[i].get(), results[j].get());
    }
  }

  // Is the same name encountered?
  for (size_t i = 0; i < names.Length(); ++i) {
    for (size_t j = i + 1; j < names.Length(); ++j) {
      ASSERT_STRNE(asWide(names[i]).c_str(), asWide(names[j]).c_str());
    }
  }
};

TEST(TestFileSystemHashSource, encodeGeneratedHash)
{
  Name expected = u"HF6FOFV72G3NMDEJKYMVRIFJO4X5ZNZCF2GM7Q4Y5Q3E7NPQKSLA"_ns;
  ASSERT_EQ(kExpectedLength, expected.Length());

  EntryId parent = "a"_ns;
  Name name = u"b"_ns;
  TEST_TRY_UNWRAP(EntryId entry,
                  FileSystemHashSource::GenerateHash(parent, name));
  ASSERT_EQ(sha256ByteLength, entry.Length());

  TEST_TRY_UNWRAP(Name result, FileSystemHashSource::EncodeHash(FileId(entry)));
  ASSERT_EQ(kExpectedLength, result.Length());
  ASSERT_STREQ(asWide(expected).c_str(), asWide(result).c_str());

  // Generate further hashes
  TEST_TRY_UNWRAP(entry, FileSystemHashSource::GenerateHash(entry, result));
  ASSERT_EQ(sha256ByteLength, entry.Length());

  TEST_TRY_UNWRAP(result, FileSystemHashSource::EncodeHash(FileId(entry)));

  // Always the same length
  ASSERT_EQ(kExpectedLength, result.Length());

  // Encoded versions should differ
  ASSERT_STRNE(asWide(expected).c_str(), asWide(result).c_str());

  // Padding length should have been stripped
  char16_t padding = u"="_ns[0];
  const int32_t paddingStart = result.FindChar(padding);
  ASSERT_EQ(-1, paddingStart);
};

}  // namespace mozilla::dom::fs::test
