/*
 * 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/memory/bytes.h"

#include <memory>
#include <utility>

#include "gtest/gtest.h"
#include "paimon/memory/memory_pool.h"

namespace paimon::test {
TEST(BytesTest, TestSimple) {
    auto pool = paimon::GetMemoryPool();
    Bytes bytes("abcde", pool.get());
    Bytes moved_bytes("efgh", pool.get());
    ASSERT_EQ(9, pool->CurrentUsage());

    moved_bytes = std::move(bytes);
    ASSERT_EQ(5, pool->CurrentUsage());
    ASSERT_EQ("abcde", std::string(moved_bytes.data(), moved_bytes.size()));
}

TEST(BytesTest, TestCopyOf) {
    auto pool = paimon::GetMemoryPool();
    // pool allocate 5 bytes + sizeof(Bytes)
    auto bytes = Bytes::AllocateBytes("abcde", pool.get());
    ASSERT_EQ("abcde", std::string(bytes->data(), bytes->size()));

    // test cp_bytes 100b and src bytes 5b
    auto cp_bytes1 = Bytes::CopyOf(*bytes, /*size=*/100, pool.get());
    ASSERT_EQ("abcde", std::string(cp_bytes1->data(), 5));
    // pool allocate 100 bytes + sizeof(Bytes)
    ASSERT_EQ(5 + 100 + sizeof(Bytes) * 2, pool->CurrentUsage());

    // test cp_bytes 2b and src bytes 5b
    auto cp_bytes2 = Bytes::CopyOf(*bytes, /*size=*/2, pool.get());
    ASSERT_EQ("ab", std::string(cp_bytes2->data(), 2));
    // pool allocate 2 bytes + sizeof(Bytes)
    ASSERT_EQ(5 + 100 + 2 + sizeof(Bytes) * 3, pool->CurrentUsage());
}

TEST(BytesTest, TestAllocateBytesAndMove) {
    auto pool = paimon::GetMemoryPool();
    // pool allocate 5 + sizeof(Bytes)
    PAIMON_UNIQUE_PTR<Bytes> bytes = Bytes::AllocateBytes("abcde", pool.get());
    ASSERT_EQ("abcde", std::string(bytes->data(), bytes->size()));
    ASSERT_EQ(5 + sizeof(Bytes), pool->CurrentUsage());

    // pool allocate 4 + sizeof(Bytes)
    PAIMON_UNIQUE_PTR<Bytes> moved_bytes = Bytes::AllocateBytes("efgh", pool.get());
    ASSERT_EQ("efgh", std::string(moved_bytes->data(), moved_bytes->size()));
    ASSERT_EQ(9 + 2 * sizeof(Bytes), pool->CurrentUsage());

    // pool deallocate sizeof(Bytes) + 4
    moved_bytes = std::move(bytes);
    ASSERT_FALSE(bytes);
    ASSERT_EQ("abcde", std::string(moved_bytes->data(), moved_bytes->size()));
    ASSERT_EQ(5 + sizeof(Bytes), pool->CurrentUsage());

    // pool allocate sizeof(Bytes) and deallocate sizeof(Bytes)
    std::shared_ptr<Bytes> shared_bytes = std::move(moved_bytes);
    ASSERT_FALSE(moved_bytes);
    ASSERT_EQ("abcde", std::string(shared_bytes->data(), shared_bytes->size()));
    ASSERT_EQ(5 + sizeof(Bytes), pool->CurrentUsage());
}

TEST(BytesTest, TestCompare) {
    auto pool = paimon::GetMemoryPool();
    PAIMON_UNIQUE_PTR<Bytes> bytes1 = Bytes::AllocateBytes("abcde", pool.get());
    PAIMON_UNIQUE_PTR<Bytes> bytes2 = Bytes::AllocateBytes("abcdf", pool.get());
    ASSERT_EQ(*bytes1, *bytes1);
    ASSERT_EQ(*bytes2, *bytes2);
    ASSERT_EQ(*bytes1, *bytes1);
    ASSERT_EQ(*bytes2, *bytes2);
    ASSERT_FALSE(*bytes1 == *bytes2);
    ASSERT_LT(*bytes1, *bytes2);
    ASSERT_FALSE(*bytes1 < *bytes1);
    ASSERT_EQ(bytes1->compare(*bytes1), 0);
    ASSERT_EQ(bytes1->compare(*bytes2), -1);
    ASSERT_EQ(bytes2->compare(*bytes1), 1);
}

// Test to verify that move assignment correctly handles memory and prevents double-free.
// Before the fix, the old implementation used memcpy + destructor which caused:
// 1. The target's original memory was freed in destructor
// 2. After memcpy, both source and target pointed to same memory
// 3. When source was "reset" via placement new, it became empty
// 4. But if move assignment was called again on the same target, the memcpy'd
//    pointer would be freed again (double-free) or memory accounting would be wrong.
TEST(BytesTest, TestMoveAssignmentNoDoubleFree) {
    auto pool = paimon::GetMemoryPool();

    // Create three Bytes objects on stack
    Bytes a("aaaa", pool.get());          // 4 bytes
    Bytes b("bb", pool.get());            // 2 bytes
    Bytes c("cccccc", pool.get());        // 6 bytes
    ASSERT_EQ(12, pool->CurrentUsage());  // 4 + 2 + 6 = 12

    // First move: b = std::move(a)
    // Should free b's original memory (2 bytes), transfer a's memory to b
    b = std::move(a);
    ASSERT_EQ(10, pool->CurrentUsage());  // 4 + 6 = 10 (b's 2 bytes freed)
    ASSERT_EQ("aaaa", std::string(b.data(), b.size()));
    // Moved-from object is expected to be empty by Bytes' contract.
    ASSERT_EQ(nullptr, a.data());  // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)
    ASSERT_EQ(0, a.size());        // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)

    // Second move: b = std::move(c)
    // Should free b's current memory (4 bytes from a), transfer c's memory to b
    // This is where the old implementation would cause issues:
    // - Old code would call destructor on b, freeing the 4 bytes
    // - Then memcpy c into b, making b point to c's 6-byte buffer
    // - Memory accounting would be wrong because Free was called on wrong data
    b = std::move(c);
    ASSERT_EQ(6, pool->CurrentUsage());  // Only c's 6 bytes remain (now owned by b)
    ASSERT_EQ("cccccc", std::string(b.data(), b.size()));
    // Moved-from object is expected to be empty by Bytes' contract.
    ASSERT_EQ(nullptr, c.data());  // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)
    ASSERT_EQ(0, c.size());        // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)

    // Self-assignment should be safe. Use an alias to avoid -Wself-move.
    Bytes* self = &b;
    b = std::move(*self);
    ASSERT_EQ(6, pool->CurrentUsage());
    ASSERT_EQ("cccccc", std::string(b.data(), b.size()));
}

// Test move assignment with heap-allocated Bytes to verify no double-free
// when combining unique_ptr semantics with move assignment
TEST(BytesTest, TestMoveAssignmentHeapAllocated) {
    auto pool = paimon::GetMemoryPool();

    auto bytes1 = Bytes::AllocateBytes("hello", pool.get());   // 5 bytes + sizeof(Bytes)
    auto bytes2 = Bytes::AllocateBytes("world!", pool.get());  // 6 bytes + sizeof(Bytes)
    size_t expected = 5 + 6 + 2 * sizeof(Bytes);
    ASSERT_EQ(expected, pool->CurrentUsage());

    // Move the content of bytes1 into bytes2's Bytes object
    // This should free "world!" (6 bytes) and transfer "hello" ownership
    *bytes2 = std::move(*bytes1);
    expected = 5 + 2 * sizeof(Bytes);  // "world!" freed, "hello" transferred
    ASSERT_EQ(expected, pool->CurrentUsage());
    ASSERT_EQ("hello", std::string(bytes2->data(), bytes2->size()));
    ASSERT_EQ(nullptr, bytes1->data());

    // Reset bytes2, which should free "hello"
    bytes2.reset();
    expected = sizeof(Bytes);  // Only bytes1's empty Bytes struct remains
    ASSERT_EQ(expected, pool->CurrentUsage());
}
}  // namespace paimon::test
