/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 "paimon/format/avro/avro_input_stream_impl.h"

#include <filesystem>
#include <fstream>
#include <string>

#include "gtest/gtest.h"
#include "paimon/fs/file_system.h"
#include "paimon/fs/local/local_file_system.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/status.h"
#include "paimon/testing/utils/testharness.h"

namespace paimon::avro::test {

TEST(AvroInputStreamImplTest, TestNext) {
    auto dir = paimon::test::UniqueTestDirectory::Create();
    std::filesystem::path file_path = dir->Str() + "/file";
    std::ofstream output_file(file_path);
    ASSERT_TRUE(output_file.is_open());
    std::string test_data = "hello world";
    output_file << test_data;
    output_file.close();

    size_t buffer_size = 10;
    std::shared_ptr<FileSystem> fs = std::make_shared<LocalFileSystem>();
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs->Open(file_path));
    ASSERT_OK_AND_ASSIGN(auto stream,
                         AvroInputStreamImpl::Create(in, buffer_size, GetDefaultPool()));
    const uint8_t* data;
    size_t size;
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 10);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), test_data.substr(0, 10));
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 1);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), test_data.substr(10, 1));
    ASSERT_FALSE(stream->next(&data, &size));
}

TEST(AvroInputStreamImplTest, TestBackup) {
    auto dir = paimon::test::UniqueTestDirectory::Create();
    std::filesystem::path file_path = dir->Str() + "/file";
    std::ofstream output_file(file_path);
    ASSERT_TRUE(output_file.is_open());
    std::string test_data = "abcdefghij";
    output_file << test_data;
    output_file.close();

    size_t buffer_size = 10;
    std::shared_ptr<FileSystem> fs = std::make_shared<LocalFileSystem>();
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs->Open(file_path));
    ASSERT_OK_AND_ASSIGN(auto stream,
                         AvroInputStreamImpl::Create(in, buffer_size, GetDefaultPool()));
    const uint8_t* data;
    size_t size;
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 10);

    stream->backup(3);
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 3);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), "hij");
    stream->backup(10);
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 10);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), test_data);
}

TEST(AvroInputStreamImplTest, TestSkip) {
    auto dir = paimon::test::UniqueTestDirectory::Create();
    std::filesystem::path file_path = dir->Str() + "/file";
    std::ofstream output_file(file_path);
    ASSERT_TRUE(output_file.is_open());
    std::string test_data = "abcdefghij";
    output_file << test_data;
    output_file.close();

    size_t buffer_size = 10;
    std::shared_ptr<FileSystem> fs = std::make_shared<LocalFileSystem>();
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs->Open(file_path));
    ASSERT_OK_AND_ASSIGN(auto stream,
                         AvroInputStreamImpl::Create(in, buffer_size, GetDefaultPool()));
    const uint8_t* data;
    size_t size;
    stream->skip(5);
    ASSERT_EQ(stream->byteCount(), 5);

    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 5);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), "fghij");
    ASSERT_THROW(stream->skip(5), ::avro::Exception);  // already eof, cannot skip more
    ASSERT_EQ(stream->byteCount(), 10);
    ASSERT_FALSE(stream->next(&data, &size));  // reach eof

    ASSERT_THROW(stream->backup(7), ::avro::Exception);  // buffer item is 5, cannot backup 7
    stream->backup(4);
    ASSERT_EQ(stream->byteCount(), 6);
    stream->skip(2);  // skip 2 bytes from the available buffer data
    ASSERT_EQ(stream->byteCount(), 8);

    // verify we can read the remaining 2 bytes from buffer
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 2);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), "ij");
    ASSERT_EQ(stream->byteCount(), 10);
}

TEST(AvroInputStreamImplTest, TestSkipWithAvailableData) {
    auto dir = paimon::test::UniqueTestDirectory::Create();
    std::filesystem::path file_path = dir->Str() + "/file";
    std::ofstream output_file(file_path);
    ASSERT_TRUE(output_file.is_open());
    std::string test_data = "abcdefghij";
    output_file << test_data;
    output_file.close();

    size_t buffer_size = 10;
    std::shared_ptr<FileSystem> fs = std::make_shared<LocalFileSystem>();
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs->Open(file_path));
    ASSERT_OK_AND_ASSIGN(auto stream,
                         AvroInputStreamImpl::Create(in, buffer_size, GetDefaultPool()));

    const uint8_t* data;
    size_t size;
    // First, load data into buffer by calling next()
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 10);
    ASSERT_EQ(stream->byteCount(), 10);

    // Now backup some data to make it available in buffer
    stream->backup(7);
    ASSERT_EQ(stream->byteCount(), 3);

    // Skip 3 bytes from the available buffer data
    stream->skip(3);
    ASSERT_EQ(stream->byteCount(), 6);

    // Verify we can read the remaining 4 bytes from buffer
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(size, 4);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), "ghij");
    ASSERT_EQ(stream->byteCount(), 10);
}

TEST(AvroInputStreamImplTest, TestSeek) {
    auto dir = paimon::test::UniqueTestDirectory::Create();
    std::filesystem::path file_path = dir->Str() + "/file";
    std::ofstream output_file(file_path);
    ASSERT_TRUE(output_file.is_open());
    std::string test_data = "abcdefghij";
    output_file << test_data;
    output_file.close();

    size_t buffer_size = 5;
    std::shared_ptr<FileSystem> fs = std::make_shared<LocalFileSystem>();
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs->Open(file_path));
    ASSERT_OK_AND_ASSIGN(auto stream,
                         AvroInputStreamImpl::Create(in, buffer_size, GetDefaultPool()));

    const uint8_t* data;
    size_t size;
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(stream->byteCount(), 5);
    ASSERT_EQ(size, 5);
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), "abcde");
    stream->seek(2);
    ASSERT_EQ(stream->byteCount(), 2);

    // after seek, buffer will be cleared, cannot backup
    ASSERT_THROW(stream->backup(2), ::avro::Exception);
    ASSERT_TRUE(stream->next(&data, &size));
    ASSERT_EQ(std::string(reinterpret_cast<const char*>(data), size), "cdefg");
    ASSERT_EQ(stream->byteCount(), 7);
    ASSERT_EQ(size, 5);
}

}  // namespace paimon::avro::test
