#include "bolt_common.hpp"
#include "communication/bolt/v1/encoder/chunked_encoder_buffer.hpp"

// aliases
using BufferT = communication::bolt::ChunkedEncoderBuffer<TestOutputStream>;

// constants
using communication::bolt::kChunkHeaderSize;
using communication::bolt::kChunkMaxDataSize;
using communication::bolt::kChunkWholeSize;

// test data
constexpr const int kTestDataSize = 100000;
uint8_t test_data[kTestDataSize];

/**
 * Verifies a single chunk. The chunk should be constructed from a header
 * (chunk size) and data. The header is a two byte long number written in big
 * endian format. Data is an array of elements from test_data whose max size is
 * 0xFFFF.
 *
 * @param data pointer on data array (array of bytes)
 * @param size of data array
 * @param offset offset from the begining of the test data
 */
void VerifyChunkOfTestData(uint8_t *data, int size, uint64_t offset = 0) {
  // first two bytes are size (big endian)
  uint8_t lower_byte = size & 0xFF;
  uint8_t higher_byte = (size & 0xFF00) >> 8;
  ASSERT_EQ(*data, higher_byte);
  ASSERT_EQ(*(data + 1), lower_byte);

  // in the data array should be size number of ones
  // the header is skipped
  for (auto i = 0; i < size; ++i) {
    ASSERT_EQ(data[i + kChunkHeaderSize], test_data[i + offset]);
  }
}

TEST(BoltChunkedEncoderBuffer, OneSmallChunk) {
  int size = 100;

  // initialize tested buffer
  TestOutputStream output_stream;
  BufferT buffer(output_stream);

  // write into buffer
  buffer.Write(test_data, size);
  buffer.Flush();

  // check the output array
  // the array should look like: [0, 100, first 100 bytes of test data]
  VerifyChunkOfTestData(output_stream.output.data(), size);
}

TEST(BoltChunkedEncoderBuffer, TwoSmallChunks) {
  int size1 = 100;
  int size2 = 200;

  // initialize tested buffer
  TestOutputStream output_stream;
  BufferT buffer(output_stream);

  // write into buffer
  buffer.Write(test_data, size1);
  buffer.Flush();
  buffer.Write(test_data + size1, size2);
  buffer.Flush();

  // check the output array
  // the output array should look like this:
  // [0, 100, first 100 bytes of test data] +
  // [0, 100, second 100 bytes of test data]
  auto data = output_stream.output.data();
  VerifyChunkOfTestData(data, size1);
  VerifyChunkOfTestData(data + kChunkHeaderSize + size1, size2, size1);
}

TEST(BoltChunkedEncoderBuffer, OneAndAHalfOfMaxChunk) {
  // initialize tested buffer
  TestOutputStream output_stream;
  BufferT buffer(output_stream);

  // write into buffer
  buffer.Write(test_data, kTestDataSize);
  buffer.Flush();

  // check the output array
  // the output array should look like this:
  // [0xFF, 0xFF, first 65535 bytes of test data,
  //  0x86, 0xA1, 34465 bytes of test data after the first 65535 bytes]
  auto output = output_stream.output.data();
  VerifyChunkOfTestData(output, kChunkMaxDataSize);
  VerifyChunkOfTestData(output + kChunkWholeSize,
                        kTestDataSize - kChunkMaxDataSize, kChunkMaxDataSize);
}

int main(int argc, char **argv) {
  InitializeData(test_data, kTestDataSize);
  google::InitGoogleLogging(argv[0]);
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}