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

#include <algorithm>

#include "apps/saved_files_service.h"
#include "base/files/file_path.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "chrome/browser/extensions/test_extension_environment.h"
#include "chrome/test/base/testing_profile.h"
#include "extensions/browser/api/file_system/saved_file_entry.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension.h"
#include "testing/gtest/include/gtest/gtest.h"

#define TRACE_CALL(expression) \
  do {                         \
    SCOPED_TRACE(#expression); \
    expression;                \
  } while (0)

using apps::SavedFilesService;
using extensions::SavedFileEntry;

namespace {

std::string GenerateId(int i) {
  return base::IntToString(i) + ":filename.ext";
}

}  // namespace

class SavedFilesServiceUnitTest : public testing::Test {
 protected:
  void SetUp() override {
    testing::Test::SetUp();
    extension_ = env_.MakeExtension(*base::test::ParseJson(
        "{"
        "  \"app\": {"
        "    \"background\": {"
        "      \"scripts\": [\"background.js\"]"
        "    }"
        "  },"
        "  \"permissions\": ["
        "    {\"fileSystem\": [\"retainEntries\"]}"
        "  ]"
        "}"));
    service_ = SavedFilesService::Get(env_.profile());
    path_ = base::FilePath(FILE_PATH_LITERAL("filename.ext"));
  }

  void TearDown() override {
    SavedFilesService::ClearMaxSequenceNumberForTest();
    SavedFilesService::ClearLruSizeForTest();
    testing::Test::TearDown();
  }

  // Check that a registered file entry has the correct value.
  void CheckEntrySequenceNumber(int id, int sequence_number) {
    std::string id_string = GenerateId(id);
    SCOPED_TRACE(id_string);
    EXPECT_TRUE(service_->IsRegistered(extension_->id(), id_string));
    const SavedFileEntry* entry =
        service_->GetFileEntry(extension_->id(), id_string);
    ASSERT_TRUE(entry);
    EXPECT_EQ(id_string, entry->id);
    EXPECT_EQ(path_, entry->path);
    EXPECT_TRUE(entry->is_directory);
    EXPECT_EQ(sequence_number, entry->sequence_number);
  }

  // Check that a range of registered file entries have the correct values.
  void CheckRangeEnqueuedInOrder(int start, int end) {
    SavedFileEntry entry;
    for (int i = start; i < end; i++) {
      CheckEntrySequenceNumber(i, i + 1);
    }
  }

  extensions::TestExtensionEnvironment env_;
  const extensions::Extension* extension_;
  SavedFilesService* service_;
  base::FilePath path_;
};

TEST_F(SavedFilesServiceUnitTest, RetainTwoFilesTest) {
  service_->RegisterFileEntry(extension_->id(), GenerateId(1), path_, true);
  service_->RegisterFileEntry(extension_->id(), GenerateId(2), path_, true);
  service_->RegisterFileEntry(extension_->id(), GenerateId(3), path_, true);

  // Test that no entry has a sequence number.
  TRACE_CALL(CheckEntrySequenceNumber(1, 0));
  TRACE_CALL(CheckEntrySequenceNumber(2, 0));
  TRACE_CALL(CheckEntrySequenceNumber(3, 0));

  // Test that only entry #1 has a sequence number.
  service_->EnqueueFileEntry(extension_->id(), GenerateId(1));
  TRACE_CALL(CheckEntrySequenceNumber(1, 1));
  TRACE_CALL(CheckEntrySequenceNumber(2, 0));

  // Test that entry #1 has not changed sequence number because it is the most
  // recently enqueued entry.
  service_->EnqueueFileEntry(extension_->id(), GenerateId(1));
  TRACE_CALL(CheckEntrySequenceNumber(1, 1));
  TRACE_CALL(CheckEntrySequenceNumber(2, 0));

  // Test that entry #1 is unchanged and entry #2 has been assigned the next
  // sequence number.
  service_->EnqueueFileEntry(extension_->id(), GenerateId(2));
  TRACE_CALL(CheckEntrySequenceNumber(1, 1));
  TRACE_CALL(CheckEntrySequenceNumber(2, 2));

  // Test that both entries #1 and #2 are unchanged because #2 is the most
  // recently enqueued entry.
  service_->EnqueueFileEntry(extension_->id(), GenerateId(2));
  TRACE_CALL(CheckEntrySequenceNumber(1, 1));
  TRACE_CALL(CheckEntrySequenceNumber(2, 2));

  // Test that entry #1 has been assigned the next sequence number.
  service_->EnqueueFileEntry(extension_->id(), GenerateId(1));
  TRACE_CALL(CheckEntrySequenceNumber(1, 3));
  TRACE_CALL(CheckEntrySequenceNumber(2, 2));
  TRACE_CALL(CheckEntrySequenceNumber(3, 0));

  EXPECT_FALSE(service_->IsRegistered(extension_->id(), "another id"));
  SavedFileEntry entry;
  EXPECT_FALSE(service_->GetFileEntry(extension_->id(), "another id"));

  // ClearQueueIfNoRetainPermission should be a no-op because the app has the
  // fileSystem.retainEntries permission.
  service_->ClearQueueIfNoRetainPermission(extension_);
  TRACE_CALL(CheckEntrySequenceNumber(1, 3));
  TRACE_CALL(CheckEntrySequenceNumber(2, 2));
  TRACE_CALL(CheckEntrySequenceNumber(3, 0));

  // Test that after a clear, retained file entries are unchanged, but file
  // entries that have been registered but not retained are no longer
  // registered.
  service_->Clear(extension_->id());
  TRACE_CALL(CheckEntrySequenceNumber(1, 3));
  TRACE_CALL(CheckEntrySequenceNumber(2, 2));
  EXPECT_FALSE(service_->IsRegistered(extension_->id(), GenerateId(3)));
}

TEST_F(SavedFilesServiceUnitTest, NoRetainEntriesPermissionTest) {
  extension_ = env_.MakeExtension(*base::test::ParseJson(
      "{\"app\": {\"background\": {\"scripts\": [\"background.js\"]}},"
      "\"permissions\": [\"fileSystem\"]}"));
  service_->RegisterFileEntry(extension_->id(), GenerateId(1), path_, true);
  TRACE_CALL(CheckEntrySequenceNumber(1, 0));
  SavedFileEntry entry;
  service_->EnqueueFileEntry(extension_->id(), GenerateId(1));
  TRACE_CALL(CheckEntrySequenceNumber(1, 1));
  EXPECT_FALSE(service_->IsRegistered(extension_->id(), "another id"));
  EXPECT_FALSE(service_->GetFileEntry(extension_->id(), "another id"));

  // ClearQueueIfNoRetainPermission should clear the queue, since the app does
  // not have the "retainEntries" permission.
  service_->ClearQueueIfNoRetainPermission(extension_);
  std::vector<SavedFileEntry> entries =
      service_->GetAllFileEntries(extension_->id());
  EXPECT_TRUE(entries.empty());
}

TEST_F(SavedFilesServiceUnitTest, EvictionTest) {
  SavedFilesService::SetLruSizeForTest(10);
  for (int i = 0; i < 10; i++) {
    service_->RegisterFileEntry(extension_->id(), GenerateId(i), path_, true);
    service_->EnqueueFileEntry(extension_->id(), GenerateId(i));
  }
  service_->RegisterFileEntry(extension_->id(), GenerateId(10), path_, true);

  // Expect that entries 0 to 9 are in the queue, but 10 is not.
  TRACE_CALL(CheckRangeEnqueuedInOrder(0, 10));
  TRACE_CALL(CheckEntrySequenceNumber(10, 0));
  service_->EnqueueFileEntry(extension_->id(), GenerateId(10));

  // Expect that entries 1 to 10 are in the queue, but entry 0 is not.
  TRACE_CALL(CheckEntrySequenceNumber(0, 0));
  TRACE_CALL(CheckRangeEnqueuedInOrder(1, 11));

  // Check that retained entries are unchanged after a clear.
  service_->Clear(extension_->id());
  SavedFileEntry entry;
  EXPECT_FALSE(service_->GetFileEntry(extension_->id(), GenerateId(0)));
  TRACE_CALL(CheckRangeEnqueuedInOrder(1, 11));

  // Expect that entry 2 is now at the back of the queue, and no further entries
  // have been evicted.
  service_->EnqueueFileEntry(extension_->id(), GenerateId(2));
  TRACE_CALL(CheckEntrySequenceNumber(2, 12));
  TRACE_CALL(CheckRangeEnqueuedInOrder(1, 1));
  TRACE_CALL(CheckRangeEnqueuedInOrder(3, 11));

  // Check that retained entries are unchanged after a clear.
  service_->Clear(extension_->id());
  TRACE_CALL(CheckEntrySequenceNumber(2, 12));
  TRACE_CALL(CheckRangeEnqueuedInOrder(1, 1));
  TRACE_CALL(CheckRangeEnqueuedInOrder(3, 11));
}

TEST_F(SavedFilesServiceUnitTest, SequenceNumberCompactionTest) {
  SavedFilesService::SetMaxSequenceNumberForTest(8);
  SavedFilesService::SetLruSizeForTest(8);
  for (int i = 0; i < 4; i++) {
    service_->RegisterFileEntry(extension_->id(), GenerateId(i), path_, true);
    service_->EnqueueFileEntry(extension_->id(), GenerateId(i));
  }
  service_->EnqueueFileEntry(extension_->id(), GenerateId(2));
  service_->EnqueueFileEntry(extension_->id(), GenerateId(3));
  service_->EnqueueFileEntry(extension_->id(), GenerateId(2));

  // The sequence numbers should be sparse, as they have not gone over the
  // limit.
  TRACE_CALL(CheckEntrySequenceNumber(0, 1));
  TRACE_CALL(CheckEntrySequenceNumber(1, 2));
  TRACE_CALL(CheckEntrySequenceNumber(2, 7));
  TRACE_CALL(CheckEntrySequenceNumber(3, 6));
  service_->Clear(extension_->id());
  TRACE_CALL(CheckEntrySequenceNumber(0, 1));
  TRACE_CALL(CheckEntrySequenceNumber(1, 2));
  TRACE_CALL(CheckEntrySequenceNumber(2, 7));
  TRACE_CALL(CheckEntrySequenceNumber(3, 6));

  // This should push the sequence number to the limit of 8, and trigger a
  // sequence number compaction. Expect that the sequence numbers are
  // contiguous from 1 to 4.
  service_->EnqueueFileEntry(extension_->id(), GenerateId(3));
  TRACE_CALL(CheckRangeEnqueuedInOrder(0, 4));
  service_->Clear(extension_->id());
  TRACE_CALL(CheckRangeEnqueuedInOrder(0, 4));
}
