// Copyright 2020 The Pigweed Authors
//
// 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
//
//     https://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 "pw_blob_store/blob_store.h"

#include <array>
#include <cstddef>
#include <cstring>

#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/fake_flash_memory.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/test_key_value_store.h"
#include "pw_log/log.h"
#include "pw_random/xor_shift.h"
#include "pw_span/span.h"
#include "pw_unit_test/framework.h"

#ifndef PW_FLASH_TEST_ALIGNMENT
#define PW_FLASH_TEST_ALIGNMENT 1
#endif

namespace pw::blob_store {
namespace {

class BlobStoreTest : public ::testing::Test {
 protected:
  static constexpr char kBlobTitle[] = "TestBlobBlock";

  BlobStoreTest() : flash_(kFlashAlignment), partition_(&flash_) {}

  void InitFlashTo(span<const std::byte> contents) {
    ASSERT_EQ(OkStatus(), partition_.Erase());
    std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
  }

  void InitSourceBufferToRandom(uint64_t seed,
                                size_t init_size_bytes = kBlobDataSize) {
    ASSERT_LE(init_size_bytes, source_buffer_.size());
    random::XorShiftStarRng64 rng(seed);

    std::memset(source_buffer_.data(),
                static_cast<int>(flash_.erased_memory_content()),
                source_buffer_.size());
    rng.Get(span(source_buffer_).first(init_size_bytes));
  }

  void InitSourceBufferToFill(char fill,
                              size_t fill_size_bytes = kBlobDataSize) {
    ASSERT_LE(fill_size_bytes, source_buffer_.size());
    std::memset(source_buffer_.data(),
                static_cast<int>(flash_.erased_memory_content()),
                source_buffer_.size());
    std::memset(source_buffer_.data(), fill, fill_size_bytes);
  }

  // Fill the source buffer with random pattern based on given seed, written to
  // BlobStore in specified chunk size.
  void WriteTestBlock(size_t write_size_bytes = kBlobDataSize) {
    ASSERT_LE(write_size_bytes, source_buffer_.size());
    constexpr size_t kBufferSize = 256;
    kvs::ChecksumCrc16 checksum;

    ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);

    BlobStoreBuffer<kBufferSize> blob(
        kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
    EXPECT_EQ(OkStatus(), blob.Init());

    BlobStore::BlobWriterWithBuffer writer(blob);
    EXPECT_EQ(OkStatus(), writer.Open());
    ASSERT_EQ(OkStatus(), writer.Write(write_data));
    EXPECT_EQ(OkStatus(), writer.Close());

    VerifyBlob(blob, write_size_bytes);
  }

  void VerifyBlob(BlobStore& blob, size_t expected_data_size) {
    EXPECT_TRUE(blob.HasData());
    BlobStore::BlobReader reader(blob);
    ASSERT_EQ(OkStatus(), reader.Open());
    EXPECT_EQ(expected_data_size, reader.ConservativeReadLimit());
    Result<ConstByteSpan> result = reader.GetMemoryMappedBlob();
    ASSERT_TRUE(result.ok());
    EXPECT_EQ(expected_data_size, result.value().size_bytes());
    VerifyFlash(result.value().first(result.value().size_bytes()));
    VerifyFlash(flash_.buffer().first(result.value().size_bytes()));
    EXPECT_EQ(OkStatus(), reader.Close());
  }

  // Open a new blob instance and read the blob using the given read chunk size.
  void ChunkReadTest(size_t read_chunk_size) {
    kvs::ChecksumCrc16 checksum;

    VerifyFlash(flash_.buffer());

    constexpr size_t kBufferSize = 16;
    BlobStoreBuffer<kBufferSize> blob(
        kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
    EXPECT_EQ(OkStatus(), blob.Init());

    // Use reader to check for valid data.
    BlobStore::BlobReader reader1(blob);
    ASSERT_EQ(OkStatus(), reader1.Open());
    Result<ConstByteSpan> possible_blob = reader1.GetMemoryMappedBlob();
    ASSERT_TRUE(possible_blob.ok());
    VerifyFlash(possible_blob.value());
    EXPECT_EQ(OkStatus(), reader1.Close());

    BlobStore::BlobReader reader(blob);
    ASSERT_EQ(OkStatus(), reader.Open());

    std::array<std::byte, kBlobDataSize> read_buffer;

    ByteSpan read_span = read_buffer;
    while (read_span.size_bytes() > 0) {
      size_t read_size = std::min(read_span.size_bytes(), read_chunk_size);

      PW_LOG_DEBUG("Do write of %u bytes, %u bytes remain",
                   static_cast<unsigned>(read_size),
                   static_cast<unsigned>(read_span.size_bytes()));

      ASSERT_EQ(read_span.size_bytes(), reader.ConservativeReadLimit());
      auto result = reader.Read(read_span.first(read_size));
      ASSERT_EQ(result.status(), OkStatus());
      read_span = read_span.subspan(read_size);
    }
    EXPECT_EQ(OkStatus(), reader.Close());

    VerifyFlash(read_buffer);
  }

  void VerifyFlash(ConstByteSpan verify_bytes, size_t offset = 0) {
    // Should be defined as same size.
    EXPECT_EQ(source_buffer_.size(), flash_.buffer().size_bytes());

    // Can't allow it to march off the end of source_buffer_.
    ASSERT_LE((verify_bytes.size_bytes() + offset), source_buffer_.size());

    for (size_t i = 0; i < verify_bytes.size_bytes(); i++) {
      ASSERT_EQ(source_buffer_[i + offset], verify_bytes[i]);
    }
  }

  static constexpr size_t kFlashAlignment = PW_FLASH_TEST_ALIGNMENT;
  static constexpr size_t kSectorSize = 1024;
  static constexpr size_t kSectorCount = 6;
  static constexpr size_t kBlobDataSize = (kSectorCount * kSectorSize);

  kvs::FakeFlashMemoryBuffer<kSectorSize, kSectorCount> flash_;
  kvs::FlashPartition partition_;
  std::array<std::byte, kBlobDataSize> source_buffer_;
};

TEST_F(BlobStoreTest, Init_Ok) {
  // TODO(davidrogers): Do init test with flash/kvs explicitly in the different
  // possible entry states.
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      "Blob_OK", partition_, nullptr, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
}

TEST_F(BlobStoreTest, Writer_ConservativeLimits) {
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      "Blob_OK", partition_, nullptr, kvs::TestKvs(), kBufferSize);
  ASSERT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer writer(blob);
  ASSERT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(writer.ConservativeReadLimit(), 0u);
  EXPECT_EQ(writer.ConservativeWriteLimit(), kSectorSize * kSectorCount);
  ASSERT_EQ(OkStatus(), writer.Close());

  BlobStore::DeferredWriterWithBuffer deferred_writer(blob);
  ASSERT_EQ(OkStatus(), deferred_writer.Open());
  EXPECT_EQ(deferred_writer.ConservativeReadLimit(), 0u);
  EXPECT_EQ(deferred_writer.ConservativeWriteLimit(), kBufferSize);
}

// Write to the blob using a flash_write_size_bytes smaller than the
// buffer size. Use Write operations smaller than flash_write_size_bytes
// to ensure it checks the internal buffering path.
TEST_F(BlobStoreTest, OversizedWriteBuffer) {
  size_t write_size_bytes = 8;
  ASSERT_LE(write_size_bytes, source_buffer_.size());
  constexpr size_t kBufferSize = 256;
  kvs::ChecksumCrc16 checksum;

  InitSourceBufferToRandom(0x123d123);

  ConstByteSpan write_data = span(source_buffer_);
  ConstByteSpan original_source = span(source_buffer_);

  EXPECT_EQ(OkStatus(), partition_.Erase());

  BlobStoreBuffer<kBufferSize> blob(
      kBlobTitle, partition_, &checksum, kvs::TestKvs(), 64);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer writer(blob);
  EXPECT_EQ(OkStatus(), writer.Open());
  while (write_data.size_bytes() > 0) {
    ASSERT_EQ(OkStatus(), writer.Write(write_data.first(write_size_bytes)));
    write_data = write_data.subspan(write_size_bytes);
  }
  EXPECT_EQ(OkStatus(), writer.Close());

  VerifyBlob(blob, original_source.size_bytes());
}

TEST_F(BlobStoreTest, Reader_ConservativeLimits) {
  InitSourceBufferToRandom(0x11309);
  WriteTestBlock();

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      "TestBlobBlock", partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
  EXPECT_TRUE(blob.HasData());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());

  EXPECT_EQ(kBlobDataSize, reader.ConservativeReadLimit());
  EXPECT_EQ(0u, reader.ConservativeWriteLimit());
}

TEST_F(BlobStoreTest, IsOpen) {
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      "Blob_open", partition_, nullptr, kvs::TestKvs(), kBufferSize);

  BlobStore::DeferredWriterWithBuffer deferred_writer(blob);
  BlobStore::BlobWriterWithBuffer regular_writer(blob);
  BlobStore::BlobReader reader(blob);
  EXPECT_EQ(Status::FailedPrecondition(), regular_writer.Open());
  EXPECT_EQ(Status::FailedPrecondition(), regular_writer.Resume().status());
  EXPECT_EQ(Status::FailedPrecondition(), reader.Open());

  EXPECT_EQ(OkStatus(), blob.Init());

  EXPECT_EQ(false, deferred_writer.IsOpen());
  EXPECT_EQ(OkStatus(), deferred_writer.Open());
  EXPECT_EQ(true, deferred_writer.IsOpen());

  EXPECT_EQ(Status::Unavailable(), regular_writer.Open());
  EXPECT_EQ(Status::Unavailable(), reader.Open());

  EXPECT_EQ(OkStatus(), deferred_writer.Close());
  EXPECT_EQ(false, deferred_writer.IsOpen());

  EXPECT_EQ(false, regular_writer.IsOpen());
  EXPECT_EQ(OkStatus(), regular_writer.Open());
  EXPECT_EQ(true, regular_writer.IsOpen());

  EXPECT_FALSE(blob.HasData());

  // Need to write something, so the blob reader is able to open.
  std::array<std::byte, 64> tmp_buffer = {};
  EXPECT_EQ(OkStatus(), regular_writer.Write(tmp_buffer));
  EXPECT_EQ(OkStatus(), regular_writer.Close());
  EXPECT_EQ(false, regular_writer.IsOpen());

  EXPECT_TRUE(blob.HasData());
  EXPECT_EQ(false, reader.IsOpen());
  ASSERT_EQ(OkStatus(), reader.Open());
  EXPECT_EQ(true, reader.IsOpen());
  EXPECT_EQ(OkStatus(), reader.Close());
  EXPECT_EQ(false, reader.IsOpen());
}

// Write to the blob using no write buffer size. Write operations must be
// multiples of flash_write_size_bytes.
TEST_F(BlobStoreTest, NoWriteBuffer_1Alignment) {
  if (kFlashAlignment > 1) {
    // Test not valid for flash alignments greater than 1.
    return;
  }

  const size_t kWriteSizeBytes = 1;
  kvs::ChecksumCrc16 checksum;

  InitSourceBufferToRandom(0xaabd123);

  ConstByteSpan write_data = span(source_buffer_);
  ConstByteSpan original_source = span(source_buffer_);

  EXPECT_EQ(OkStatus(), partition_.Erase());

  BlobStore blob(kBlobTitle,
                 partition_,
                 &checksum,
                 kvs::TestKvs(),
                 span<std::byte>(),
                 kWriteSizeBytes);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer writer(blob);
  EXPECT_EQ(OkStatus(), writer.Open());

  size_t test_write_size[] = {1, 1, 2, 4, 32, 128};

  for (size_t size : test_write_size) {
    ASSERT_EQ(OkStatus(), writer.Write(write_data.first(size)));
    write_data = write_data.subspan(size);
  }

  while (write_data.size_bytes() > 0) {
    const size_t finish_write_size = 8;
    ASSERT_EQ(OkStatus(), writer.Write(write_data.first(finish_write_size)));
    write_data = write_data.subspan(finish_write_size);
  }
  EXPECT_EQ(write_data.size_bytes(), 0U);
  EXPECT_EQ(OkStatus(), writer.Close());

  VerifyBlob(blob, original_source.size_bytes());
}

// Write to the blob using no write buffer size. Write operations must be
// multiples of flash_write_size_bytes.
TEST_F(BlobStoreTest, NoWriteBuffer_16Alignment) {
  if (kFlashAlignment > 16) {
    // Test not valid for flash alignments greater than 16.
    return;
  }

  const size_t kWriteSizeBytes = 16;
  kvs::ChecksumCrc16 checksum;

  InitSourceBufferToRandom(0x6745d123);

  ConstByteSpan write_data = span(source_buffer_);
  ConstByteSpan original_source = span(source_buffer_);

  EXPECT_EQ(OkStatus(), partition_.Erase());

  BlobStore blob(kBlobTitle,
                 partition_,
                 &checksum,
                 kvs::TestKvs(),
                 span<std::byte>(),
                 kWriteSizeBytes);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer writer(blob);
  EXPECT_EQ(OkStatus(), writer.Open());
  ASSERT_EQ(Status::InvalidArgument(), writer.Write(write_data.first(1)));
  ASSERT_EQ(Status::InvalidArgument(),
            writer.Write(write_data.first(kWriteSizeBytes / 2)));

  ASSERT_EQ(OkStatus(), writer.Write(write_data.first(4 * kWriteSizeBytes)));
  write_data = write_data.subspan(4 * kWriteSizeBytes);

  ASSERT_EQ(Status::InvalidArgument(), writer.Write(write_data.first(1)));
  ASSERT_EQ(Status::InvalidArgument(),
            writer.Write(write_data.first(kWriteSizeBytes / 2)));

  while (write_data.size_bytes() > 0) {
    ASSERT_EQ(OkStatus(), writer.Write(write_data.first(kWriteSizeBytes)));
    write_data = write_data.subspan(kWriteSizeBytes);
  }
  EXPECT_EQ(OkStatus(), writer.Close());

  VerifyBlob(blob, original_source.size_bytes());
}

TEST_F(BlobStoreTest, FileName) {
  InitSourceBufferToRandom(0x8675309);
  WriteTestBlock();
  constexpr std::string_view kFileName("my_file_1.bin");
  std::array<std::byte, 64> tmp_buffer = {};
  static_assert(kFileName.size() <= tmp_buffer.size());
  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  {
    // Create/init a blob store in a nested scope so it can be re-initialized
    // later when checking the read.
    BlobStoreBuffer<kBufferSize> blob(
        kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
    EXPECT_EQ(OkStatus(), blob.Init());

    BlobStore::BlobWriterWithBuffer<kFileName.size()> writer(blob);

    EXPECT_EQ(OkStatus(), writer.Open());
    EXPECT_EQ(OkStatus(), writer.SetFileName(kFileName));
    EXPECT_EQ(OkStatus(), writer.Write(tmp_buffer));

    // Read back filename from the open writer.
    memset(tmp_buffer.data(), 0, tmp_buffer.size());
    StatusWithSize sws = writer.GetFileName(
        {reinterpret_cast<char*>(tmp_buffer.data()), tmp_buffer.size()});
    EXPECT_EQ(OkStatus(), sws.status());
    ASSERT_EQ(kFileName.size(), sws.size());
    EXPECT_EQ(memcmp(kFileName.data(), tmp_buffer.data(), kFileName.size()), 0);
    // END of read back filename from the open writer.

    EXPECT_EQ(OkStatus(), writer.Close());
    EXPECT_EQ(OkStatus(),
              kvs::TestKvs().acquire()->Get(kBlobTitle, tmp_buffer).status());
  }

  BlobStoreBuffer<kBufferSize> blob(
      kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  // Ensure the file name can be read from a reader.
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());

  memset(tmp_buffer.data(), 0, tmp_buffer.size());
  StatusWithSize sws = reader.GetFileName(
      {reinterpret_cast<char*>(tmp_buffer.data()), tmp_buffer.size()});

  EXPECT_EQ(OkStatus(), sws.status());
  ASSERT_EQ(kFileName.size(), sws.size());
  EXPECT_EQ(memcmp(kFileName.data(), tmp_buffer.data(), kFileName.size()), 0);
}

TEST_F(BlobStoreTest, FileNameUndersizedRead) {
  InitSourceBufferToRandom(0x8675309);
  WriteTestBlock();
  constexpr std::string_view kFileName("my_file_1.bin");
  std::array<std::byte, 4> tmp_buffer = {};
  static_assert(kFileName.size() > tmp_buffer.size());

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer<kFileName.size()> writer(blob);

  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(OkStatus(), writer.SetFileName(kFileName));
  EXPECT_EQ(OkStatus(), writer.Write(as_bytes(span("some interesting data"))));
  EXPECT_EQ(OkStatus(), writer.Close());

  // Ensure the file name can be read from a reader.
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());

  StatusWithSize sws = reader.GetFileName(
      {reinterpret_cast<char*>(tmp_buffer.data()), tmp_buffer.size()});
  EXPECT_EQ(Status::ResourceExhausted(), sws.status());
  ASSERT_EQ(tmp_buffer.size(), sws.size());
  EXPECT_EQ(memcmp(kFileName.data(), tmp_buffer.data(), sws.size()), 0);
}

TEST_F(BlobStoreTest, FileNameUndersizedSet) {
  InitSourceBufferToRandom(0x8675309);
  WriteTestBlock();
  constexpr std::string_view kFileName("my_file_1.bin");
  std::array<std::byte, 64> tmp_buffer = {};
  std::array<std::byte, 64> zero_buffer = {};
  static_assert(kFileName.size() <= tmp_buffer.size());

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer<2> writer(blob);

  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(Status::ResourceExhausted(), writer.SetFileName(kFileName));

  // Read back filename from the open writer.
  memset(tmp_buffer.data(), 0, tmp_buffer.size());
  memset(zero_buffer.data(), 0, tmp_buffer.size());
  StatusWithSize sws = writer.GetFileName(
      {reinterpret_cast<char*>(tmp_buffer.data()), tmp_buffer.size()});
  EXPECT_EQ(Status::NotFound(), sws.status());
  ASSERT_EQ(0U, sws.size());
  EXPECT_EQ(memcmp(zero_buffer.data(), tmp_buffer.data(), tmp_buffer.size()),
            0);
  // END of read back filename from the open writer.

  EXPECT_EQ(OkStatus(), writer.Close());
}

TEST_F(BlobStoreTest, FileNameInvalidation) {
  InitSourceBufferToRandom(0x8675309);
  WriteTestBlock();

  constexpr std::string_view kFileName("sliced_cheese.png");
  std::array<std::byte, 64> tmp_buffer = {};
  static_assert(kFileName.size() <= tmp_buffer.size());

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer<kFileName.size()> writer(blob);

  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(OkStatus(), writer.SetFileName(kFileName));
  EXPECT_EQ(OkStatus(), writer.Write(tmp_buffer));
  EXPECT_EQ(OkStatus(), writer.Discard());
  EXPECT_EQ(OkStatus(), writer.Write(tmp_buffer));

  // Read back filename from the open writer.
  {
    StatusWithSize sws = writer.GetFileName(
        {reinterpret_cast<char*>(tmp_buffer.data()), tmp_buffer.size()});
    EXPECT_EQ(Status::NotFound(), sws.status());
    ASSERT_EQ(0u, sws.size());
  }

  EXPECT_EQ(OkStatus(), writer.Close());

  // Check that the file name was discarded by Discard().
  memset(tmp_buffer.data(), 0, tmp_buffer.size());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());
  StatusWithSize sws = reader.GetFileName(
      {reinterpret_cast<char*>(tmp_buffer.data()), tmp_buffer.size()});
  EXPECT_EQ(Status::NotFound(), sws.status());
  ASSERT_EQ(0u, sws.size());
}

TEST_F(BlobStoreTest, NoFileName) {
  InitSourceBufferToRandom(0x8675309);
  WriteTestBlock();

  std::array<std::byte, 64> tmp_buffer = {};
  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  // Ensure blobs with no file names work as expected.
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());

  StatusWithSize sws = reader.GetFileName(
      {reinterpret_cast<char*>(tmp_buffer.data()), tmp_buffer.size()});
  EXPECT_EQ(Status::NotFound(), sws.status());
  ASSERT_EQ(0u, sws.size());
}

TEST_F(BlobStoreTest, V1MetadataBackwardsCompatible) {
  constexpr size_t kWriteSize = 25;
  WriteTestBlock(kWriteSize);

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  // Read the written data in the current format.
  internal::BlobMetadataHeader current_metadata;
  ASSERT_EQ(OkStatus(),
            kvs::TestKvs().acquire()->Get(kBlobTitle, &current_metadata));

  // Re-save only the V1 metadata contents.
  ASSERT_EQ(
      OkStatus(),
      kvs::TestKvs().acquire()->Put(kBlobTitle, current_metadata.v1_metadata));

  // Ensure the BlobStore's contents aren't invalid.
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());
  ASSERT_EQ(kWriteSize, reader.ConservativeReadLimit());
  ASSERT_EQ(current_metadata.v1_metadata.data_size_bytes,
            reader.ConservativeReadLimit());
}

TEST_F(BlobStoreTest, Discard) {
  InitSourceBufferToRandom(0x8675309);
  WriteTestBlock();
  constexpr char blob_title[] = "TestBlobBlock";
  std::array<std::byte, 64> tmp_buffer = {};

  kvs::ChecksumCrc16 checksum;

  // TODO(davidrogers): Do this test with flash/kvs in the different entry state
  // combinations.

  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      blob_title, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  EXPECT_TRUE(blob.HasData());

  BlobStore::BlobWriterWithBuffer writer(blob);

  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(OkStatus(), writer.Write(tmp_buffer));

  // Blob should NOT be valid to read, because the write data was only buffered,
  // and has not been written to flash yet.
  EXPECT_FALSE(blob.HasData());

  // The write does an implicit erase so there should be no key for this blob.
  EXPECT_EQ(Status::NotFound(),
            kvs::TestKvs().acquire()->Get(blob_title, tmp_buffer).status());
  EXPECT_EQ(OkStatus(), writer.Close());

  EXPECT_TRUE(blob.HasData());

  EXPECT_EQ(OkStatus(),
            kvs::TestKvs().acquire()->Get(blob_title, tmp_buffer).status());

  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(OkStatus(), writer.Discard());
  EXPECT_EQ(OkStatus(), writer.Close());

  EXPECT_FALSE(blob.HasData());

  EXPECT_EQ(Status::NotFound(),
            kvs::TestKvs().acquire()->Get(blob_title, tmp_buffer).status());
}

TEST_F(BlobStoreTest, MultipleErase) {
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      "Blob_OK", partition_, nullptr, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer writer(blob);
  EXPECT_EQ(OkStatus(), writer.Open());

  EXPECT_EQ(OkStatus(), writer.Erase());
  EXPECT_EQ(OkStatus(), writer.Erase());
  EXPECT_EQ(OkStatus(), writer.Erase());
}

TEST_F(BlobStoreTest, OffsetRead) {
  InitSourceBufferToRandom(0x11309);
  WriteTestBlock();

  constexpr size_t kOffset = 10;
  ASSERT_LT(kOffset, kBlobDataSize);

  kvs::ChecksumCrc16 checksum;

  char name[16] = "TestBlobBlock";
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      name, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open(kOffset));

  std::array<std::byte, kBlobDataSize - kOffset> read_buffer;
  ByteSpan read_span = read_buffer;
  ASSERT_EQ(read_span.size_bytes(), reader.ConservativeReadLimit());

  auto result = reader.Read(read_span);
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(OkStatus(), reader.Close());
  VerifyFlash(read_buffer, kOffset);
}

TEST_F(BlobStoreTest, SeekOffsetRead) {
  InitSourceBufferToRandom(0x11309);
  WriteTestBlock();

  constexpr size_t kOffset = 10;
  ASSERT_LT(kOffset, kBlobDataSize);

  kvs::ChecksumCrc16 checksum;

  char name[16] = "TestBlobBlock";
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      name, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());
  ASSERT_EQ(OkStatus(), reader.Seek(kOffset));

  std::array<std::byte, kBlobDataSize - kOffset> read_buffer;
  ByteSpan read_span = read_buffer;
  ASSERT_EQ(read_span.size_bytes(), reader.ConservativeReadLimit());

  auto result = reader.Read(read_span);
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(OkStatus(), reader.Close());
  VerifyFlash(read_buffer, kOffset);
}

TEST_F(BlobStoreTest, InvalidReadOffset) {
  InitSourceBufferToRandom(0x11309);
  WriteTestBlock();

  constexpr size_t kOffset = kBlobDataSize;

  kvs::ChecksumCrc16 checksum;

  char name[16] = "TestBlobBlock";
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      name, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(Status::InvalidArgument(), reader.Open(kOffset));
}

TEST_F(BlobStoreTest, ReadSeekClosedReader) {
  InitSourceBufferToRandom(0x11309);
  WriteTestBlock();

  kvs::ChecksumCrc16 checksum;

  char name[16] = "TestBlobBlock";
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      name, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());
  ASSERT_EQ(OkStatus(), reader.Close());

  EXPECT_EQ(Status::FailedPrecondition(), reader.Seek(0));

  std::byte read_buffer[32];
  EXPECT_EQ(Status::FailedPrecondition(), reader.Read(read_buffer).status());
}

TEST_F(BlobStoreTest, InvalidSeekOffset) {
  InitSourceBufferToRandom(0x11309);
  WriteTestBlock();

  constexpr size_t kOffset = kBlobDataSize;

  kvs::ChecksumCrc16 checksum;

  char name[16] = "TestBlobBlock";
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      name, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());
  ASSERT_EQ(Status::OutOfRange(), reader.Seek(kOffset));
}

// Write a block to blob and close with part of a write buffer with unflushed
// data.
TEST_F(BlobStoreTest, WriteBufferWithRemainderInBuffer) {
  InitSourceBufferToRandom(0x11309);

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      "TestBlobBlock", partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  const size_t write_size_bytes = kBlobDataSize - 10;
  ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);

  BlobStore::BlobWriterWithBuffer writer(blob);
  EXPECT_EQ(OkStatus(), writer.Open());
  ASSERT_EQ(OkStatus(), writer.Write(write_data));
  EXPECT_EQ(OkStatus(), writer.Close());

  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());
  EXPECT_EQ(write_size_bytes, reader.ConservativeReadLimit());
}

// Test reading with a read buffer larger than the available data in the blob.
TEST_F(BlobStoreTest, ReadBufferIsLargerThanData) {
  InitSourceBufferToRandom(0x57326);

  constexpr size_t kWriteBytes = 64;
  WriteTestBlock(kWriteBytes);

  kvs::ChecksumCrc16 checksum;

  char name[16] = "TestBlobBlock";
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      name, partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());
  BlobStore::BlobReader reader(blob);
  ASSERT_EQ(OkStatus(), reader.Open());
  EXPECT_EQ(kWriteBytes, reader.ConservativeReadLimit());

  std::array<std::byte, kWriteBytes + 10> read_buffer;
  ByteSpan read_span = read_buffer;

  auto result = reader.Read(read_span);
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(OkStatus(), reader.Close());
}

TEST_F(BlobStoreTest, ChunkRead1) {
  InitSourceBufferToRandom(0x8675309);
  WriteTestBlock();
  ChunkReadTest(1);
}

TEST_F(BlobStoreTest, ChunkRead3) {
  InitSourceBufferToFill(0);
  WriteTestBlock();
  ChunkReadTest(3);
}

TEST_F(BlobStoreTest, ChunkRead4) {
  InitSourceBufferToFill(1);
  WriteTestBlock();
  ChunkReadTest(4);
}

TEST_F(BlobStoreTest, ChunkRead5) {
  InitSourceBufferToFill(0xff);
  WriteTestBlock();
  ChunkReadTest(5);
}

TEST_F(BlobStoreTest, ChunkRead16) {
  InitSourceBufferToRandom(0x86);
  WriteTestBlock();
  ChunkReadTest(16);
}

TEST_F(BlobStoreTest, ChunkRead64) {
  InitSourceBufferToRandom(0x9);
  WriteTestBlock();
  ChunkReadTest(64);
}

TEST_F(BlobStoreTest, ChunkReadFull) {
  InitSourceBufferToRandom(0x9);
  WriteTestBlock();
  ChunkReadTest(kBlobDataSize);
}

TEST_F(BlobStoreTest, PartialBufferThenClose) {
  // Do write of only a partial chunk, which will only have bytes in buffer
  // (none written to flash) at close.
  size_t data_bytes = 12;
  InitSourceBufferToRandom(0x111, data_bytes);
  WriteTestBlock(data_bytes);

  // Do write with several full chunks and then some partial.
  data_bytes = 158;
  InitSourceBufferToRandom(0x3222, data_bytes);
}

// Test to do write/close, write/close multiple times.
TEST_F(BlobStoreTest, MultipleWrites) {
  InitSourceBufferToRandom(0x1121);
  WriteTestBlock();
  InitSourceBufferToRandom(0x515);
  WriteTestBlock();
  InitSourceBufferToRandom(0x4321);
  WriteTestBlock();
}

TEST_F(BlobStoreTest, ResumeEmptyAbandonBlob) {
  InitSourceBufferToRandom(0x11309);

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      "TestBlobBlock", partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  BlobStore::BlobWriterWithBuffer writer(blob);
  BlobStore::BlobWriterWithBuffer second_writer(blob);
  BlobStore::BlobReader reader(blob);

  EXPECT_EQ(OkStatus(), writer.Open());

  EXPECT_EQ(OkStatus(), writer.Erase());
  EXPECT_EQ(OkStatus(), writer.Abandon());
  EXPECT_FALSE(blob.HasData());

  StatusWithSize resume_sws = writer.Resume();
  EXPECT_EQ(OkStatus(), resume_sws.status());
  EXPECT_EQ(0U, resume_sws.size());

  EXPECT_EQ(Status::Unavailable(), reader.Open());
  EXPECT_EQ(Status::Unavailable(), second_writer.Open());
  EXPECT_EQ(Status::Unavailable(), second_writer.Resume().status());

  EXPECT_EQ(OkStatus(), writer.Abandon());
}

TEST_F(BlobStoreTest, ResumePartialAbandonBlob) {
  InitSourceBufferToRandom(0x11309);

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> blob(
      "TestBlobBlock", partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  // The test fills 1/2 sector size less than the full blob size. Resume will
  // backup 1.5 sectors for the resume point. Make sure the blob has enough
  // sectors to do all that.
  ASSERT_GE(kSectorCount, 3U);

  const size_t write_size_bytes = kBlobDataSize - (kSectorSize / 2);
  ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);

  BlobStore::BlobWriterWithBuffer writer(blob);
  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(OkStatus(), writer.Write(write_data));
  EXPECT_EQ(OkStatus(), writer.Abandon());
  EXPECT_FALSE(blob.HasData());

  StatusWithSize resume_sws = writer.Resume();
  EXPECT_EQ(OkStatus(), resume_sws.status());
  const size_t expected_resume_size = (kSectorCount - 2) * kSectorSize;
  EXPECT_EQ(expected_resume_size, resume_sws.size());
  StatusWithSize eod_sws = partition_.EndOfWrittenData();
  EXPECT_EQ(OkStatus(), eod_sws.status());
  EXPECT_EQ(eod_sws.size(), resume_sws.size());
  VerifyFlash(flash_.buffer().first(resume_sws.size()));

  EXPECT_EQ(OkStatus(),
            writer.Write(span(source_buffer_).subspan(resume_sws.size())));
  EXPECT_EQ(kBlobDataSize, writer.CurrentSizeBytes());
  EXPECT_EQ(OkStatus(), writer.Close());

  VerifyBlob(blob, kBlobDataSize);
}

TEST_F(BlobStoreTest, ResumePartialAfterRebootBlob) {
  InitSourceBufferToRandom(0x11309);

  kvs::ChecksumCrc16 first_checksum;
  constexpr size_t kBufferSize = 16;
  BlobStoreBuffer<kBufferSize> first_blob("TestBlobBlock",
                                          partition_,
                                          &first_checksum,
                                          kvs::TestKvs(),
                                          kBufferSize);
  EXPECT_EQ(OkStatus(), first_blob.Init());

  // The test fills 1/2 sector size less than the full blob size. Resume will
  // backup 1.5 sectors for the resume point. Make sure the blob has enough
  // sectors to do all that.
  ASSERT_GE(kSectorCount, 3U);

  const size_t write_size_bytes = kBlobDataSize - (kSectorSize / 2);
  ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);

  // Write the data using
  BlobStore::BlobWriterWithBuffer writer(first_blob);
  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(OkStatus(), writer.Write(write_data));

  // Use a new blob and writer for the resume to simulate having
  // crashed/rebooted and starting fresh.

  kvs::ChecksumCrc16 second_checksum;
  BlobStoreBuffer<kBufferSize> second_blob("TestBlobBlock",
                                           partition_,
                                           &second_checksum,
                                           kvs::TestKvs(),
                                           kBufferSize);
  EXPECT_EQ(OkStatus(), second_blob.Init());
  BlobStore::BlobWriterWithBuffer second_writer(second_blob);

  StatusWithSize resume_sws = second_writer.Resume();
  EXPECT_EQ(OkStatus(), resume_sws.status());
  const size_t expected_resume_size = (kSectorCount - 2) * kSectorSize;
  EXPECT_EQ(expected_resume_size, resume_sws.size());
  StatusWithSize eod_sws = partition_.EndOfWrittenData();
  EXPECT_EQ(OkStatus(), eod_sws.status());
  EXPECT_EQ(eod_sws.size(), resume_sws.size());
  VerifyFlash(flash_.buffer().first(resume_sws.size()));

  EXPECT_EQ(
      OkStatus(),
      second_writer.Write(span(source_buffer_).subspan(resume_sws.size())));
  EXPECT_EQ(kBlobDataSize, second_writer.CurrentSizeBytes());
  EXPECT_EQ(OkStatus(), second_writer.Close());

  VerifyBlob(second_blob, kBlobDataSize);
}

TEST_F(BlobStoreTest, ResumeAbandonBlobBackedUpToZero) {
  InitSourceBufferToRandom(0x11309);

  kvs::ChecksumCrc16 checksum;
  constexpr size_t kBufferSize = 256;
  BlobStoreBuffer<kBufferSize> blob(
      "TestBlobBlock", partition_, &checksum, kvs::TestKvs(), kBufferSize);
  EXPECT_EQ(OkStatus(), blob.Init());

  // The test fills 1/2 sector size less than the full blob size. Resume will
  // backup 1.5 sectors for the resume point. Make sure the blob has enough
  // sectors to do all that.
  ASSERT_GE(kSectorCount, 3U);

  const size_t write_size_bytes = kSectorSize + (kSectorSize / 2);
  ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);

  // Write 1.5 sectors, should resume with zero bytes.
  BlobStore::BlobWriterWithBuffer writer(blob);
  EXPECT_EQ(OkStatus(), writer.Open());
  EXPECT_EQ(OkStatus(), writer.Write(write_data));
  EXPECT_EQ(OkStatus(), writer.Abandon());
  StatusWithSize resume_sws = writer.Resume();
  EXPECT_EQ(OkStatus(), resume_sws.status());
  EXPECT_EQ(0U, resume_sws.size());

  // Write 0.5 sectors, should resume with zero bytes.
  EXPECT_EQ(OkStatus(), writer.Erase());
  EXPECT_EQ(OkStatus(), writer.Write(write_data.first(kSectorSize / 2)));
  EXPECT_EQ(OkStatus(), writer.Abandon());
  resume_sws = writer.Resume();
  EXPECT_EQ(OkStatus(), resume_sws.status());
  EXPECT_EQ(0U, resume_sws.size());
}
}  // namespace
}  // namespace pw::blob_store
