// Copyright 2023 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_multibuf/multibuf_v1.h"

#include "pw_assert/check.h"
#include "pw_bytes/array.h"
#include "pw_bytes/suffix.h"
#include "pw_multibuf_private/test_utils.h"
#include "pw_span/span.h"
#include "pw_unit_test/framework.h"

namespace pw::multibuf {
namespace {

using namespace pw::multibuf::test_utils;

#if __cplusplus >= 202002L
static_assert(std::forward_iterator<MultiBuf::iterator>);
static_assert(std::forward_iterator<MultiBuf::const_iterator>);
static_assert(std::forward_iterator<MultiBufChunks::iterator>);
static_assert(std::forward_iterator<MultiBufChunks::const_iterator>);
#endif  // __cplusplus >= 202002L

static_assert(
    sizeof(MultiBufChunks) == sizeof(MultiBuf),
    "MultiBuf is a byte view of MultiBufChunks and does not add members");

TEST(MultiBuf, IsDefaultConstructible) { [[maybe_unused]] MultiBuf buf; }

TEST(MultiBuf, WithOneChunkReleases) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  const auto& metrics = allocator.metrics();
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
  EXPECT_EQ(metrics.num_allocations.value(), 2U);
  buf.Release();
  EXPECT_EQ(metrics.num_deallocations.value(), 2U);
}

TEST(MultiBuf, WithOneChunkReleasesOnDestruction) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  const auto& metrics = allocator.metrics();
  {
    MultiBuf buf;
    buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
    EXPECT_EQ(metrics.num_allocations.value(), 2U);
  }
  EXPECT_EQ(metrics.num_deallocations.value(), 2U);
}

TEST(MultiBuf, WithMultipleChunksReleasesAllOnDestruction) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  const auto& metrics = allocator.metrics();
  {
    MultiBuf buf;
    buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
    buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
    EXPECT_EQ(metrics.num_allocations.value(), 4U);
  }
  EXPECT_EQ(metrics.num_deallocations.value(), 4U);
}

TEST(MultiBuf, SizeReturnsNumberOfBytes) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  EXPECT_EQ(buf.size(), 0U);
  buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
  EXPECT_EQ(buf.size(), kArbitraryChunkSize);
  buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
  EXPECT_EQ(buf.size(), kArbitraryChunkSize * 2);
}

TEST(MultiBuf, EmptyIfNoChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  EXPECT_EQ(buf.size(), 0U);
  EXPECT_TRUE(buf.empty());
  buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
  EXPECT_NE(buf.size(), 0U);
  EXPECT_FALSE(buf.empty());
}

TEST(MultiBuf, EmptyIfOnlyEmptyChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  EXPECT_TRUE(buf.empty());
  buf.PushFrontChunk(MakeChunk(allocator, 0));
  EXPECT_TRUE(buf.empty());
  buf.PushFrontChunk(MakeChunk(allocator, 0));
  EXPECT_TRUE(buf.empty());
  EXPECT_EQ(buf.size(), 0U);
}

TEST(MultiBuf, EmptyIsFalseIfAnyNonEmptyChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 0));
  EXPECT_TRUE(buf.empty());
  buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));
  EXPECT_FALSE(buf.empty());
  EXPECT_EQ(buf.size(), kArbitraryChunkSize);
}

TEST(MultiBuf, ClaimPrefixReclaimsFirstChunkPrefix) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  OwnedChunk chunk = MakeChunk(allocator, 16);
  chunk->DiscardPrefix(7);
  buf.PushFrontChunk(std::move(chunk));
  EXPECT_EQ(buf.size(), 9U);
  EXPECT_EQ(buf.ClaimPrefix(7), true);
  EXPECT_EQ(buf.size(), 16U);
}

TEST(MultiBuf, ClaimPrefixOnFirstChunkWithoutPrefixReturnsFalse) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 16));
  EXPECT_EQ(buf.size(), 16U);
  EXPECT_EQ(buf.ClaimPrefix(7), false);
  EXPECT_EQ(buf.size(), 16U);
}

TEST(MultiBuf, ClaimPrefixWithoutChunksReturnsFalse) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  EXPECT_EQ(buf.size(), 0U);
  EXPECT_EQ(buf.ClaimPrefix(7), false);
  EXPECT_EQ(buf.size(), 0U);
}

TEST(MultiBuf, ClaimSuffixReclaimsLastChunkSuffix) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  OwnedChunk chunk = MakeChunk(allocator, 16U);
  chunk->Truncate(9U);
  buf.PushFrontChunk(std::move(chunk));
  buf.PushFrontChunk(MakeChunk(allocator, 4U));
  EXPECT_EQ(buf.size(), 13U);
  EXPECT_EQ(buf.ClaimSuffix(7U), true);
  EXPECT_EQ(buf.size(), 20U);
}

TEST(MultiBuf, ClaimSuffixOnLastChunkWithoutSuffixReturnsFalse) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 16U));
  EXPECT_EQ(buf.size(), 16U);
  EXPECT_EQ(buf.ClaimPrefix(7U), false);
  EXPECT_EQ(buf.size(), 16U);
}

TEST(MultiBuf, ClaimSuffixWithoutChunksReturnsFalse) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  EXPECT_EQ(buf.size(), 0U);
  EXPECT_EQ(buf.ClaimSuffix(7U), false);
  EXPECT_EQ(buf.size(), 0U);
}

TEST(MultiBuf, DiscardPrefixWithZeroDoesNothing) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.DiscardPrefix(0);
  EXPECT_EQ(buf.size(), 0U);
}

TEST(MultiBuf, DiscardPrefixDiscardsPartialChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 16U));
  buf.DiscardPrefix(5U);
  EXPECT_EQ(buf.size(), 11U);
}

TEST(MultiBuf, DiscardPrefixDiscardsWholeChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 16U));
  buf.PushFrontChunk(MakeChunk(allocator, 3U));
  buf.DiscardPrefix(16U);
  EXPECT_EQ(buf.size(), 3U);
}

TEST(MultiBuf, DiscardPrefixDiscardsMultipleChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 16U));
  buf.PushFrontChunk(MakeChunk(allocator, 4U));
  buf.PushFrontChunk(MakeChunk(allocator, 3U));
  buf.DiscardPrefix(21U);
  EXPECT_EQ(buf.size(), 2U);
}

TEST(MultiBuf, SliceDiscardsPrefixAndSuffixWholeAndPartialChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 1_b, 1_b}));
  buf.PushBackChunk(MakeChunk(allocator, {2_b, 2_b, 2_b}));
  buf.PushBackChunk(MakeChunk(allocator, {3_b, 3_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 4_b, 4_b}));
  buf.Slice(4, 7);
  ExpectElementsEqual(buf, {2_b, 2_b, 3_b});
}

TEST(MultiBuf, SliceDoesNotModifyChunkMemory) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  std::array<std::byte, 4> kBytes = {1_b, 2_b, 3_b, 4_b};
  OwnedChunk chunk = MakeChunk(allocator, kBytes);
  ConstByteSpan span(chunk);
  buf.PushFrontChunk(std::move(chunk));
  buf.Slice(2, 3);
  ExpectElementsEqual(span, kBytes);
}

TEST(MultiBuf, TruncateRemovesFinalEmptyChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 3U));
  buf.PushFrontChunk(MakeChunk(allocator, 3U));
  buf.Truncate(3U);
  EXPECT_EQ(buf.size(), 3U);
  EXPECT_EQ(buf.Chunks().size(), 1U);
}

TEST(MultiBuf, TruncateRemovesWholeAndPartialChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 3U));
  buf.PushFrontChunk(MakeChunk(allocator, 3U));
  buf.Truncate(2U);
  EXPECT_EQ(buf.size(), 2U);
}

TEST(MultiBuf, TruncateAfterRemovesWholeAndPartialChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushFrontChunk(MakeChunk(allocator, 3U));
  buf.PushFrontChunk(MakeChunk(allocator, 0U));
  buf.PushFrontChunk(MakeChunk(allocator, 1U));
  auto it = buf.begin();
  ++it;
  buf.TruncateAfter(it);
  EXPECT_EQ(buf.size(), 2U);
}

TEST(MultiBuf, TruncateEmptyBuffer) {
  MultiBuf buf;
  buf.Truncate(0);
  EXPECT_TRUE(buf.empty());
}

TEST(MultiBuf, TakePrefixWithNoBytesDoesNothing) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  std::optional<MultiBuf> empty_front = buf.TakePrefix(0);
  ASSERT_TRUE(empty_front.has_value());
  EXPECT_EQ(buf.size(), 0U);
  EXPECT_EQ(empty_front->size(), 0U);
}

TEST(MultiBuf, TakePrefixReturnsPartialChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  std::optional<MultiBuf> old_front = buf.TakePrefix(2);
  ASSERT_TRUE(old_front.has_value());
  ExpectElementsEqual(*old_front, {1_b, 2_b});
  ExpectElementsEqual(buf, {3_b});
}

TEST(MultiBuf, TakePrefixReturnsWholeAndPartialChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));
  std::optional<MultiBuf> old_front = buf.TakePrefix(4);
  ASSERT_TRUE(old_front.has_value());
  ExpectElementsEqual(*old_front, {1_b, 2_b, 3_b, 4_b});
  ExpectElementsEqual(buf, {5_b, 6_b});
}

TEST(MultiBuf, TakeSuffixReturnsWholeAndPartialChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));
  std::optional<MultiBuf> old_tail = buf.TakeSuffix(4);
  ASSERT_TRUE(old_tail.has_value());
  ExpectElementsEqual(buf, {1_b, 2_b});
  ExpectElementsEqual(*old_tail, {3_b, 4_b, 5_b, 6_b});
}

TEST(MultiBuf, PushPrefixPrependsData) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));
  MultiBuf buf2;
  buf2.PushBackChunk(MakeChunk(allocator, {7_b, 8_b}));
  buf2.PushPrefix(std::move(buf));
  ExpectElementsEqual(buf2, {1_b, 2_b, 3_b, 4_b, 5_b, 6_b, 7_b, 8_b});
}

TEST(MultiBuf, PushSuffixAppendsData) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));
  MultiBuf buf2;
  buf2.PushBackChunk(MakeChunk(allocator, {7_b, 8_b}));
  buf2.PushSuffix(std::move(buf));
  ExpectElementsEqual(buf2, {7_b, 8_b, 1_b, 2_b, 3_b, 4_b, 5_b, 6_b});
}

TEST(MultiBuf, PushFrontChunkAddsBytesToFront) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;

  const std::array<std::byte, 3> kBytesOne = {0_b, 1_b, 2_b};
  auto chunk_one = MakeChunk(allocator, kBytesOne);
  buf.PushFrontChunk(std::move(chunk_one));
  ExpectElementsEqual(buf, kBytesOne);

  const std::array<std::byte, 4> kBytesTwo = {9_b, 10_b, 11_b, 12_b};
  auto chunk_two = MakeChunk(allocator, kBytesTwo);
  buf.PushFrontChunk(std::move(chunk_two));

  // clang-format off
  ExpectElementsEqual(buf, {
      9_b, 10_b, 11_b, 12_b,
      0_b, 1_b, 2_b,
  });
  // clang-format on
}

TEST(MultiBuf, InsertChunkOnEmptyBufAddsFirstChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;

  const std::array<std::byte, 3> kBytes = {0_b, 1_b, 2_b};
  auto chunk = MakeChunk(allocator, kBytes);
  auto inserted_iter = buf.InsertChunk(buf.Chunks().begin(), std::move(chunk));
  EXPECT_EQ(inserted_iter, buf.Chunks().begin());
  ExpectElementsEqual(buf, kBytes);
  EXPECT_EQ(++inserted_iter, buf.Chunks().end());
}

TEST(MultiBuf, InsertChunkAtEndOfBufAddsLastChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;

  // Add a chunk to the beginning
  buf.PushFrontChunk(MakeChunk(allocator, kArbitraryChunkSize));

  const std::array<std::byte, 3> kBytes = {0_b, 1_b, 2_b};
  auto chunk = MakeChunk(allocator, kBytes);
  auto inserted_iter = buf.InsertChunk(buf.Chunks().end(), std::move(chunk));
  EXPECT_EQ(inserted_iter, ++buf.Chunks().begin());
  EXPECT_EQ(++inserted_iter, buf.Chunks().end());
  const Chunk& second_chunk = *(++buf.Chunks().begin());
  ExpectElementsEqual(second_chunk, kBytes);
}

TEST(MultiBuf, TakeChunkAtBeginRemovesAndReturnsFirstChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  auto insert_iter = buf.Chunks().begin();
  insert_iter = buf.InsertChunk(insert_iter, MakeChunk(allocator, 2));
  insert_iter = buf.InsertChunk(++insert_iter, MakeChunk(allocator, 4));

  auto [chunk_iter, chunk] = buf.TakeChunk(buf.Chunks().begin());
  EXPECT_EQ(chunk.size(), 2U);
  EXPECT_EQ(chunk_iter->size(), 4U);
  ++chunk_iter;
  EXPECT_EQ(chunk_iter, buf.Chunks().end());
}

TEST(MultiBuf, TakeChunkOnLastInsertedIterReturnsLastInserted) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  auto iter = buf.Chunks().begin();
  iter = buf.InsertChunk(iter, MakeChunk(allocator, 42));
  iter = buf.InsertChunk(++iter, MakeChunk(allocator, 11));
  iter = buf.InsertChunk(++iter, MakeChunk(allocator, 65));
  OwnedChunk chunk;
  std::tie(iter, chunk) = buf.TakeChunk(iter);
  EXPECT_EQ(iter, buf.Chunks().end());
  EXPECT_EQ(chunk.size(), 65U);
}

TEST(MultiBuf, RangeBasedForLoopsCompile) {
  MultiBuf buf;
  for ([[maybe_unused]] std::byte& byte : buf) {
  }
  for ([[maybe_unused]] const std::byte& byte : buf) {
  }
  for ([[maybe_unused]] Chunk& chunk : buf.Chunks()) {
  }
  for ([[maybe_unused]] const Chunk& chunk : buf.Chunks()) {
  }

  const MultiBuf const_buf;
  for ([[maybe_unused]] const std::byte& byte : const_buf) {
  }
  for ([[maybe_unused]] const Chunk& chunk : const_buf.Chunks()) {
  }
}

TEST(MultiBuf, IteratorAdvancesNAcrossChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));

  MultiBuf::iterator iter = buf.begin();
  iter += 4;
  EXPECT_EQ(*iter, 5_b);
}

TEST(MultiBuf, IteratorAdvancesNAcrossZeroLengthChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, 0));
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, 0));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));

  MultiBuf::iterator iter = buf.begin();
  iter += 4;
  EXPECT_EQ(*iter, 5_b);
}

TEST(MultiBuf, ConstIteratorAdvancesNAcrossChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));

  MultiBuf::const_iterator iter = buf.cbegin();
  iter += 4;
  EXPECT_EQ(*iter, 5_b);
}

TEST(MultiBuf, IteratorSkipsEmptyChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, 0));
  buf.PushBackChunk(MakeChunk(allocator, 0));
  buf.PushBackChunk(MakeChunk(allocator, {1_b}));
  buf.PushBackChunk(MakeChunk(allocator, 0));
  buf.PushBackChunk(MakeChunk(allocator, {2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, 0));

  MultiBuf::iterator it = buf.begin();
  ASSERT_EQ(*it++, 1_b);
  ASSERT_EQ(*it++, 2_b);
  ASSERT_EQ(*it++, 3_b);
  ASSERT_EQ(it, buf.end());
}

constexpr auto kSequentialBytes =
    bytes::Initialized<6>([](size_t i) { return i + 1; });

TEST(MultiBuf, CopyToFromEmptyMultiBuf) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  std::array<std::byte, 6> buffer = {};
  StatusWithSize result = buf.CopyTo(buffer);
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(result.size(), 0u);

  result = buf.CopyTo({});
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(result.size(), 0u);
}

TEST(MultiBuf, CopyToEmptyDestination) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b, 4_b}));
  StatusWithSize result = buf.CopyTo({});
  ASSERT_EQ(result.status(), Status::ResourceExhausted());
  EXPECT_EQ(result.size(), 0u);
}

TEST(MultiBuf, CopyToOneChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b, 2_b, 3_b, 4_b}));

  std::array<std::byte, 4> buffer = {};
  StatusWithSize result = buf.CopyTo(buffer);
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(result.size(), 4u);
  EXPECT_TRUE(
      std::equal(buffer.begin(), buffer.end(), kSequentialBytes.begin()));
}

TEST(MultiBuf, CopyToVariousChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b}));
  buf.PushBackChunk(MakeChunk(allocator, {2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));

  std::array<std::byte, 6> buffer = {};
  StatusWithSize result = buf.CopyTo(buffer);
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(result.size(), 6u);
  EXPECT_TRUE(
      std::equal(buffer.begin(), buffer.end(), kSequentialBytes.begin()));
}

TEST(MultiBuf, CopyToInTwoParts) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;

  constexpr size_t kMultiBufSize = 6;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b}));
  buf.PushBackChunk(MakeChunk(allocator, {2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {}));
  buf.PushBackChunk(MakeChunk(allocator, {4_b, 5_b, 6_b}));
  ASSERT_EQ(buf.size(), kMultiBufSize);

  for (size_t first = 0; first < kMultiBufSize; ++first) {
    std::array<std::byte, kMultiBufSize> buffer = {};
    StatusWithSize result = buf.CopyTo(span(buffer).first(first));
    ASSERT_EQ(result.status(), Status::ResourceExhausted());
    ASSERT_EQ(result.size(), first);

    result = buf.CopyTo(span(buffer).last(kMultiBufSize - first),
                        result.size());  // start from last offset
    ASSERT_EQ(result.status(), OkStatus());
    ASSERT_EQ(result.size(), kMultiBufSize - first);

    ASSERT_TRUE(
        std::equal(buffer.begin(), buffer.end(), kSequentialBytes.begin()))
        << "The whole buffer should have copied";
  }
}

TEST(MultiBuf, CopyToPositionIsEnd) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b}));
  buf.PushBackChunk(MakeChunk(allocator, {2_b, 3_b}));
  buf.PushBackChunk(MakeChunk(allocator, {}));

  StatusWithSize result = buf.CopyTo({}, 3u);
  ASSERT_EQ(result.status(), OkStatus());
  EXPECT_EQ(result.size(), 0u);
}

TEST(MultiBuf, CopyFromIntoOneChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf mb;
  mb.PushBackChunk(MakeChunk(allocator, 6));

  StatusWithSize result = mb.CopyFrom(kSequentialBytes);
  EXPECT_EQ(result.status(), OkStatus());
  ASSERT_EQ(result.size(), 6u);
  EXPECT_TRUE(std::equal(mb.begin(), mb.end(), kSequentialBytes.begin()));
}

TEST(MultiBuf, CopyFromIntoMultipleChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf mb;
  mb.PushBackChunk(MakeChunk(allocator, 2));
  mb.PushBackChunk(MakeChunk(allocator, 0));
  mb.PushBackChunk(MakeChunk(allocator, 3));
  mb.PushBackChunk(MakeChunk(allocator, 1));
  mb.PushBackChunk(MakeChunk(allocator, 0));

  StatusWithSize result = mb.CopyFrom(kSequentialBytes);
  EXPECT_EQ(result.status(), OkStatus());
  ASSERT_EQ(result.size(), 6u);
  EXPECT_TRUE(std::equal(mb.begin(), mb.end(), kSequentialBytes.begin()));
}

TEST(MultiBuf, CopyFromInTwoParts) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;

  for (size_t first = 0; first < kSequentialBytes.size(); ++first) {
    MultiBuf mb;
    mb.PushBackChunk(MakeChunk(allocator, 1));
    mb.PushBackChunk(MakeChunk(allocator, 0));
    mb.PushBackChunk(MakeChunk(allocator, 0));
    mb.PushBackChunk(MakeChunk(allocator, 2));
    mb.PushBackChunk(MakeChunk(allocator, 3));
    ASSERT_EQ(mb.size(), kSequentialBytes.size());

    StatusWithSize result = mb.CopyFrom(span(kSequentialBytes).first(first));
    ASSERT_EQ(result.status(), OkStatus());
    ASSERT_EQ(result.size(), first);

    result = mb.CopyFrom(
        span(kSequentialBytes).last(kSequentialBytes.size() - first),
        result.size());  // start from last offset
    ASSERT_EQ(result.status(), OkStatus());
    ASSERT_EQ(result.size(), kSequentialBytes.size() - first);

    ASSERT_TRUE(std::equal(mb.begin(), mb.end(), kSequentialBytes.begin()))
        << "The whole buffer should have copied";
  }
}

TEST(MultiBuf, CopyFromAndTruncate) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;

  for (size_t to_copy = 0; to_copy < kSequentialBytes.size(); ++to_copy) {
    MultiBuf mb;
    mb.PushBackChunk(MakeChunk(allocator, 1));
    mb.PushBackChunk(MakeChunk(allocator, 0));
    mb.PushBackChunk(MakeChunk(allocator, 0));
    mb.PushBackChunk(MakeChunk(allocator, 2));
    mb.PushBackChunk(MakeChunk(allocator, 3));
    mb.PushBackChunk(MakeChunk(allocator, 0));
    ASSERT_EQ(mb.size(), kSequentialBytes.size());

    StatusWithSize result =
        mb.CopyFromAndTruncate(span(kSequentialBytes).first(to_copy));
    ASSERT_EQ(result.status(), OkStatus());
    ASSERT_EQ(result.size(), to_copy);
    ASSERT_EQ(mb.size(), result.size());
    ASSERT_TRUE(std::equal(mb.begin(), mb.end(), kSequentialBytes.begin()));
  }
}

TEST(MultiBuf, CopyFromAndTruncateFromOffset) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;

  static constexpr std::array<std::byte, 6> kZeroes = {};

  // Sweep offsets 0–6 (inclusive), and copy 0–all bytes for each offset.
  for (size_t offset = 0; offset <= kSequentialBytes.size(); ++offset) {
    for (size_t to_copy = 0; to_copy <= kSequentialBytes.size() - offset;
         ++to_copy) {
      MultiBuf mb;
      mb.PushBackChunk(MakeChunk(allocator, 2));
      mb.PushBackChunk(MakeChunk(allocator, 0));
      mb.PushBackChunk(MakeChunk(allocator, 3));
      mb.PushBackChunk(MakeChunk(allocator, 0));
      mb.PushBackChunk(MakeChunk(allocator, 0));
      mb.PushBackChunk(MakeChunk(allocator, 1));
      ASSERT_EQ(mb.size(), kSequentialBytes.size());

      StatusWithSize result =
          mb.CopyFromAndTruncate(span(kSequentialBytes).first(to_copy), offset);
      ASSERT_EQ(result.status(), OkStatus());
      ASSERT_EQ(result.size(), to_copy);
      ASSERT_EQ(mb.size(), offset + to_copy);

      // MultiBuf contains to_copy 0s followed by to_copy sequential bytes.
      ASSERT_TRUE(std::equal(mb.begin(), mb.begin() + offset, kZeroes.begin()));
      ASSERT_TRUE(
          std::equal(mb.begin() + offset, mb.end(), kSequentialBytes.begin()));
    }
  }
}

TEST(MultiBuf, CopyFromIntoEmptyMultibuf) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  MultiBuf mb;

  StatusWithSize result = mb.CopyFrom({});
  EXPECT_EQ(result.status(), OkStatus());  // empty source, so copy succeeded
  EXPECT_EQ(result.size(), 0u);

  result = mb.CopyFrom(kSequentialBytes);
  EXPECT_EQ(result.status(), Status::ResourceExhausted());
  EXPECT_EQ(result.size(), 0u);

  mb.PushBackChunk(MakeChunk(allocator, 0));  // add an empty chunk

  result = mb.CopyFrom({});
  EXPECT_EQ(result.status(), OkStatus());  // empty source, so copy succeeded
  EXPECT_EQ(result.size(), 0u);

  result = mb.CopyFrom(kSequentialBytes);
  EXPECT_EQ(result.status(), Status::ResourceExhausted());
  EXPECT_EQ(result.size(), 0u);
}

TEST(MultiBuf, IsContiguousTrueForEmptyBuffer) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;

  MultiBuf buf;
  EXPECT_TRUE(buf.IsContiguous());

  buf.PushBackChunk(MakeChunk(allocator, {}));
  EXPECT_TRUE(buf.IsContiguous());
  buf.PushBackChunk(MakeChunk(allocator, {}));
  EXPECT_TRUE(buf.IsContiguous());
  buf.PushBackChunk(MakeChunk(allocator, {}));
  EXPECT_TRUE(buf.IsContiguous());
}

TEST(MultiBuf, IsContiguousTrueForSingleNonEmptyChunk) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;

  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b}));
  EXPECT_TRUE(buf.IsContiguous());
  buf.PushBackChunk(MakeChunk(allocator, {}));
  EXPECT_TRUE(buf.IsContiguous());
  buf.PushFrontChunk(MakeChunk(allocator, {}));
  EXPECT_TRUE(buf.IsContiguous());
}

TEST(MultiBuf, IsContiguousFalseIfMultipleNonEmptyChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;

  MultiBuf buf;
  buf.PushBackChunk(MakeChunk(allocator, {1_b}));
  buf.PushBackChunk(MakeChunk(allocator, {2_b}));
  EXPECT_FALSE(buf.IsContiguous());
}

TEST(MultiBuf, ContiguousSpanAcrossMultipleChunks) {
  AllocatorForTest<kArbitraryAllocatorSize> allocator;
  OwnedChunk chunk_1 = MakeChunk(allocator, 10);
  const ConstByteSpan contiguous_span = chunk_1;
  OwnedChunk chunk_2 = chunk_1->TakeSuffix(5).value();
  OwnedChunk chunk_3 = chunk_2->TakeSuffix(5).value();
  OwnedChunk chunk_4 = chunk_3->TakeSuffix(1).value();

  MultiBuf buf;
  buf.PushBackChunk(std::move(chunk_1));       // 5 bytes
  buf.PushBackChunk(std::move(chunk_2));       // 0 bytes
  buf.PushBackChunk(std::move(chunk_3));       // 4 bytes
  buf.PushBackChunk(std::move(chunk_4));       // 1 byte
  buf.PushBackChunk(MakeChunk(allocator, 0));  // empty

  auto it = buf.Chunks().begin();
  ASSERT_EQ((it++)->size(), 5u);
  ASSERT_EQ((it++)->size(), 0u);
  ASSERT_EQ((it++)->size(), 4u);
  ASSERT_EQ((it++)->size(), 1u);
  ASSERT_EQ((it++)->size(), 0u);
  ASSERT_EQ(it, buf.Chunks().end());

  EXPECT_TRUE(buf.IsContiguous());
  ByteSpan span = buf.ContiguousSpan().value();
  EXPECT_EQ(span.data(), contiguous_span.data());
  EXPECT_EQ(span.size(), contiguous_span.size());

  it = buf.Chunks().begin();
  buf.InsertChunk(++it, MakeChunk(allocator, 1));
  EXPECT_FALSE(buf.IsContiguous());
}

}  // namespace
}  // namespace pw::multibuf
