//
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include "update_engine/payload_consumer/cached_file_descriptor.h"

#include <fcntl.h>

#include <algorithm>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include "update_engine/common/test_utils.h"
#include "update_engine/common/utils.h"

using chromeos_update_engine::test_utils::ExpectVectorsEq;
using std::min;
using std::string;
using std::vector;

namespace chromeos_update_engine {

namespace {
const size_t kCacheSize = 100;
const size_t kFileSize = 1024;
const size_t kRandomIterations = 1000;
}  // namespace

class CachedFileDescriptorTest : public ::testing::Test {
 public:
  void Open() {
    cfd_.reset(new CachedFileDescriptor(fd_, kCacheSize));
    EXPECT_TRUE(cfd_->Open(temp_file_.path().c_str(), O_RDWR, 0600));
  }

  void Write(uint8_t* buffer, size_t count) {
    size_t total_bytes_wrote = 0;
    while (total_bytes_wrote < count) {
      auto bytes_wrote =
          cfd_->Write(buffer + total_bytes_wrote, count - total_bytes_wrote);
      ASSERT_NE(bytes_wrote, -1);
      total_bytes_wrote += bytes_wrote;
    }
  }

  void Close() { EXPECT_TRUE(cfd_->Close()); }

  void SetUp() override {
    brillo::Blob zero_blob(kFileSize, 0);
    EXPECT_TRUE(utils::WriteFile(
        temp_file_.path().c_str(), zero_blob.data(), zero_blob.size()));
    Open();
  }

  void TearDown() override {
    Close();
    EXPECT_FALSE(cfd_->IsOpen());
  }

 protected:
  FileDescriptorPtr fd_{new EintrSafeFileDescriptor};
  test_utils::ScopedTempFile temp_file_{"CachedFileDescriptor-file.XXXXXX"};
  int value_{1};
  FileDescriptorPtr cfd_;
};

TEST_F(CachedFileDescriptorTest, IsOpenTest) {
  EXPECT_TRUE(cfd_->IsOpen());
}

TEST_F(CachedFileDescriptorTest, SimpleWriteTest) {
  EXPECT_EQ(cfd_->Seek(0, SEEK_SET), 0);
  brillo::Blob blob_in(kFileSize, value_);
  Write(blob_in.data(), blob_in.size());
  EXPECT_TRUE(cfd_->Flush());

  brillo::Blob blob_out;
  EXPECT_TRUE(utils::ReadFile(temp_file_.path(), &blob_out));
  EXPECT_EQ(blob_in, blob_out);
}

TEST_F(CachedFileDescriptorTest, OneBytePerWriteTest) {
  EXPECT_EQ(cfd_->Seek(0, SEEK_SET), 0);
  brillo::Blob blob_in(kFileSize, value_);
  for (size_t idx = 0; idx < blob_in.size(); idx++) {
    Write(&blob_in[idx], 1);
  }
  EXPECT_TRUE(cfd_->Flush());

  brillo::Blob blob_out;
  EXPECT_TRUE(utils::ReadFile(temp_file_.path(), &blob_out));
  EXPECT_EQ(blob_in, blob_out);
}

TEST_F(CachedFileDescriptorTest, RandomWriteTest) {
  EXPECT_EQ(cfd_->Seek(0, SEEK_SET), 0);

  brillo::Blob blob_in(kFileSize, 0);
  srand(time(nullptr));
  uint32_t rand_seed;
  for (size_t idx = 0; idx < kRandomIterations; idx++) {
    // zero to full size available.
    size_t start = rand_r(&rand_seed) % blob_in.size();
    size_t size = rand_r(&rand_seed) % (blob_in.size() - start);
    std::fill_n(&blob_in[start], size, idx % 256);
    EXPECT_EQ(cfd_->Seek(start, SEEK_SET), static_cast<off64_t>(start));
    Write(&blob_in[start], size);
  }
  EXPECT_TRUE(cfd_->Flush());

  brillo::Blob blob_out;
  EXPECT_TRUE(utils::ReadFile(temp_file_.path(), &blob_out));
  EXPECT_EQ(blob_in, blob_out);
}

TEST_F(CachedFileDescriptorTest, SeekTest) {
  EXPECT_EQ(cfd_->Seek(0, SEEK_SET), 0);
  EXPECT_EQ(cfd_->Seek(1, SEEK_SET), 1);
  EXPECT_EQ(cfd_->Seek(kFileSize - 1, SEEK_SET),
            static_cast<off64_t>(kFileSize - 1));
  EXPECT_EQ(cfd_->Seek(kFileSize, SEEK_SET), static_cast<off64_t>(kFileSize));
  EXPECT_EQ(cfd_->Seek(kFileSize + 1, SEEK_SET),
            static_cast<off64_t>(kFileSize + 1));

  EXPECT_EQ(cfd_->Seek(0, SEEK_SET), 0);
  EXPECT_EQ(cfd_->Seek(1, SEEK_CUR), 1);
  EXPECT_EQ(cfd_->Seek(1, SEEK_CUR), 2);
  EXPECT_EQ(cfd_->Seek(kFileSize - 1, SEEK_SET),
            static_cast<off64_t>(kFileSize - 1));
  EXPECT_EQ(cfd_->Seek(1, SEEK_CUR), static_cast<off64_t>(kFileSize));
  EXPECT_EQ(cfd_->Seek(1, SEEK_CUR), static_cast<off64_t>(kFileSize + 1));
}

TEST_F(CachedFileDescriptorTest, NoFlushTest) {
  EXPECT_EQ(cfd_->Seek(0, SEEK_SET), 0);
  brillo::Blob blob_in(kFileSize, value_);
  Write(blob_in.data(), blob_in.size());

  brillo::Blob blob_out;
  EXPECT_TRUE(utils::ReadFile(temp_file_.path(), &blob_out));
  EXPECT_NE(blob_in, blob_out);
}

TEST_F(CachedFileDescriptorTest, CacheSizeWriteTest) {
  off64_t seek = 10;
  brillo::Blob blob_in(kFileSize, 0);
  std::fill_n(&blob_in[seek], kCacheSize, value_);
  // We are writing exactly one cache size; Then it should be commited.
  EXPECT_EQ(cfd_->Seek(seek, SEEK_SET), seek);
  Write(&blob_in[seek], kCacheSize);

  brillo::Blob blob_out;
  EXPECT_TRUE(utils::ReadFile(temp_file_.path(), &blob_out));
  EXPECT_EQ(blob_in, blob_out);
}

TEST_F(CachedFileDescriptorTest, UnderCacheSizeWriteTest) {
  off64_t seek = 100;
  size_t less_than_cache_size = kCacheSize - 1;
  EXPECT_EQ(cfd_->Seek(seek, SEEK_SET), seek);
  brillo::Blob blob_in(kFileSize, 0);
  std::fill_n(&blob_in[seek], less_than_cache_size, value_);
  // We are writing less than one cache size; then it should not be commited.
  Write(&blob_in[seek], less_than_cache_size);

  // Revert the changes in |blob_in|.
  std::fill_n(&blob_in[seek], less_than_cache_size, 0);
  brillo::Blob blob_out;
  EXPECT_TRUE(utils::ReadFile(temp_file_.path(), &blob_out));
  EXPECT_EQ(blob_in, blob_out);
}

TEST_F(CachedFileDescriptorTest, SeekAfterWriteTest) {
  off64_t seek = 100;
  size_t less_than_cache_size = kCacheSize - 3;
  EXPECT_EQ(cfd_->Seek(seek, SEEK_SET), seek);
  brillo::Blob blob_in(kFileSize, 0);
  std::fill_n(&blob_in[seek], less_than_cache_size, value_);
  // We are writing less than  one cache size; then it should not be commited.
  Write(&blob_in[seek], less_than_cache_size);

  // Then we seek, it should've written the cache after seek.
  EXPECT_EQ(cfd_->Seek(200, SEEK_SET), 200);

  brillo::Blob blob_out;
  EXPECT_TRUE(utils::ReadFile(temp_file_.path(), &blob_out));
  EXPECT_EQ(blob_in, blob_out);
}

}  // namespace chromeos_update_engine