// Copyright 2022 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <utility>

#include "dawn/common/Math.h"
#include "dawn/native/Blob.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace dawn::native {
namespace {

// Test that a blob starts empty.
TEST(BlobTests, DefaultEmpty) {
    Blob b;
    EXPECT_TRUE(b.Empty());
    EXPECT_EQ(b.Data(), nullptr);
    EXPECT_EQ(b.Size(), 0u);
}

// Test that you can create a blob with a size in bytes and write/read its contents.
TEST(BlobTests, SizedCreation) {
    Blob b = CreateBlob(10);
    EXPECT_FALSE(b.Empty());
    EXPECT_EQ(b.Size(), 10u);
    ASSERT_NE(b.Data(), nullptr);
    // We should be able to copy 10 bytes into the blob.
    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
    memcpy(b.Data(), data, sizeof(data));
    // And retrieve the exact contents back.
    EXPECT_EQ(memcmp(b.Data(), data, sizeof(data)), 0);
}

// Test that you can create a zero-sized blob.
TEST(BlobTests, EmptySizedCreation) {
    Blob b = CreateBlob(0);
    EXPECT_TRUE(b.Empty());
    EXPECT_EQ(b.Data(), nullptr);
    EXPECT_EQ(b.Size(), 0u);
}

// Test that you can create a blob with UnsafeCreateWithDeleter, and the deleter is
// called on destruction.
TEST(BlobTests, UnsafeCreateWithDeleter) {
    unsigned char data[13] = "hello world!";
    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
    {
        // Make a blob with a mock deleter.
        Blob b = Blob::UnsafeCreateWithDeleter(data, sizeof(data), [&] { mockDeleter.Call(); });
        // Check the contents.
        EXPECT_FALSE(b.Empty());
        EXPECT_EQ(b.Size(), sizeof(data));
        ASSERT_EQ(b.Data(), data);
        EXPECT_EQ(memcmp(b.Data(), data, sizeof(data)), 0);

        // |b| is deleted when this scope exits.
        EXPECT_CALL(mockDeleter, Call());
    }
}

// Test that you can create a blob with UnsafeCreateWithDeleter with zero size but non-null data.
// The deleter is still called on destruction, and the blob is normalized to be empty.
TEST(BlobTests, UnsafeCreateWithDeleterZeroSize) {
    unsigned char data[13] = "hello world!";
    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
    {
        // Make a blob with a mock deleter.
        Blob b = Blob::UnsafeCreateWithDeleter(data, 0, [&] { mockDeleter.Call(); });
        // Check the contents.
        EXPECT_TRUE(b.Empty());
        EXPECT_EQ(b.Size(), 0u);
        // Data still points to the data.
        EXPECT_EQ(b.Data(), data);

        // |b| is deleted when this scope exits.
        EXPECT_CALL(mockDeleter, Call());
    }
}

// Test that you can create a blob with UnsafeCreateWithDeleter that points to nothing.
// The deleter should still be called.
TEST(BlobTests, UnsafeCreateWithDeleterEmpty) {
    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
    {
        // Make a blob with a mock deleter.
        Blob b = Blob::UnsafeCreateWithDeleter(nullptr, 0, [&] { mockDeleter.Call(); });
        // Check the contents.
        EXPECT_TRUE(b.Empty());
        EXPECT_EQ(b.Size(), 0u);
        EXPECT_EQ(b.Data(), nullptr);

        // |b| is deleted when this scope exits.
        EXPECT_CALL(mockDeleter, Call());
    }
}

// Test that move construction moves the data from one blob into the new one.
TEST(BlobTests, MoveConstruct) {
    // Create the blob.
    Blob b1 = CreateBlob(10);
    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
    memcpy(b1.Data(), data, sizeof(data));

    // Move construct b2 from b1.
    Blob b2(std::move(b1));

    // Data should be moved.
    EXPECT_FALSE(b2.Empty());
    EXPECT_EQ(b2.Size(), 10u);
    ASSERT_NE(b2.Data(), nullptr);
    EXPECT_EQ(memcmp(b2.Data(), data, sizeof(data)), 0);
}

// Test that move assignment moves the data from one blob into another.
TEST(BlobTests, MoveAssign) {
    // Create the blob.
    Blob b1 = CreateBlob(10);
    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
    memcpy(b1.Data(), data, sizeof(data));

    // Move assign b2 from b1.
    Blob b2;
    b2 = std::move(b1);

    // Data should be moved.
    EXPECT_FALSE(b2.Empty());
    EXPECT_EQ(b2.Size(), 10u);
    ASSERT_NE(b2.Data(), nullptr);
    EXPECT_EQ(memcmp(b2.Data(), data, sizeof(data)), 0);
}

// Test that move assignment can replace the contents of the moved-to blob.
TEST(BlobTests, MoveAssignOver) {
    // Create the blob.
    Blob b1 = CreateBlob(10);
    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
    memcpy(b1.Data(), data, sizeof(data));

    // Create another blob with a mock deleter.
    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
    Blob b2 = Blob::UnsafeCreateWithDeleter(nullptr, 0, [&] { mockDeleter.Call(); });

    // Move b1 into b2, replacing b2's contents, and expect the deleter to be called.
    EXPECT_CALL(mockDeleter, Call());
    b2 = std::move(b1);

    // Data should be moved.
    EXPECT_FALSE(b2.Empty());
    EXPECT_EQ(b2.Size(), 10u);
    ASSERT_NE(b2.Data(), nullptr);
    EXPECT_EQ(memcmp(b2.Data(), data, sizeof(data)), 0);
}

// Test that an empty blob can be requested to have a particular alignment.
TEST(BlobTests, EmptyAlignTo) {
    for (size_t alignment : {1, 2, 4, 8, 16, 32}) {
        Blob b;
        EXPECT_TRUE(b.Empty());
        EXPECT_EQ(b.Size(), 0u);
        EXPECT_EQ(b.Data(), nullptr);
        b.AlignTo(alignment);
        // After aligning, it is still empty.
        EXPECT_TRUE(b.Empty());
        EXPECT_EQ(b.Size(), 0u);
        EXPECT_EQ(b.Data(), nullptr);
    }
}

// Test that AlignTo makes a blob have a particular alignment.
TEST(BlobTests, AlignTo) {
    uint8_t data[64];
    for (uint8_t i = 0; i < sizeof(data); ++i) {
        data[i] = i;
    }
    // Test multiple alignments.
    for (size_t alignment : {1, 2, 4, 8, 16, 32}) {
        for (size_t offset = 0; offset < alignment; ++offset) {
            // Make a blob pointing to |data| starting at |offset|.
            size_t size = sizeof(data) - offset;
            testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
            Blob b =
                Blob::UnsafeCreateWithDeleter(&data[offset], size, [&] { mockDeleter.Call(); });
            bool alreadyAligned = IsPtrAligned(&data[offset], alignment);

            // The backing store should be deleted at the end of the scope, or because it was
            // replaced.
            EXPECT_CALL(mockDeleter, Call());

            b.AlignTo(alignment);
            if (!alreadyAligned) {
                // If the Blob is not aligned, its data will be deleted and replaced by AlignTo.
                testing::Mock::VerifyAndClearExpectations(&mockDeleter);
            }
            // The blob should not have changed in size.
            EXPECT_EQ(b.Size(), size) << "alignment = " << alignment << " offset = " << offset;
            // The data should be aligned.
            EXPECT_TRUE(IsPtrAligned(b.Data(), alignment))
                << "alignment = " << alignment << " offset = " << offset;
            // The contents should be the same.
            EXPECT_EQ(memcmp(b.Data(), &data[offset], size), 0)
                << "alignment = " << alignment << " offset = " << offset;
            // If the data was already aligned, the blob should point to the same memory.
            EXPECT_EQ(alreadyAligned, b.Data() == &data[offset])
                << "alignment = " << alignment << " offset = " << offset;
        }
    }
}

}  // namespace
}  // namespace dawn::native
