/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * 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
 *
 *      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 <thread>

#include <gtest/gtest.h>

#include <utils/SystemClock.h>

#include <VehicleHalTypes.h>
#include <VehicleObjectPool.h>
#include <VehicleUtils.h>

namespace android {
namespace hardware {
namespace automotive {
namespace vehicle {

namespace {

using ::aidl::android::hardware::automotive::vehicle::VehicleProperty;
using ::aidl::android::hardware::automotive::vehicle::VehiclePropertyType;
using ::aidl::android::hardware::automotive::vehicle::VehiclePropValue;

struct TestPropertyTypeInfo {
    VehiclePropertyType type;
    bool recyclable;
    size_t vecSize;
};

std::vector<TestPropertyTypeInfo> getAllPropertyTypes() {
    return {
            {
                    .type = VehiclePropertyType::INT32,
                    .recyclable = true,
                    .vecSize = 1,
            },
            {
                    .type = VehiclePropertyType::INT64,
                    .recyclable = true,
                    .vecSize = 1,
            },
            {
                    .type = VehiclePropertyType::FLOAT,
                    .recyclable = true,
                    .vecSize = 1,
            },
            {
                    .type = VehiclePropertyType::INT32_VEC,
                    .recyclable = true,
                    .vecSize = 4,
            },
            {
                    .type = VehiclePropertyType::INT64_VEC,
                    .recyclable = true,
                    .vecSize = 4,
            },
            {
                    .type = VehiclePropertyType::FLOAT_VEC,
                    .recyclable = true,
                    .vecSize = 4,
            },
            {
                    .type = VehiclePropertyType::BYTES,
                    .recyclable = true,
                    .vecSize = 4,
            },
            {
                    .type = VehiclePropertyType::INT32_VEC,
                    .recyclable = false,
                    .vecSize = 5,
            },
            {
                    .type = VehiclePropertyType::INT64_VEC,
                    .recyclable = false,
                    .vecSize = 5,
            },
            {
                    .type = VehiclePropertyType::FLOAT_VEC,
                    .recyclable = false,
                    .vecSize = 5,
            },
            {
                    .type = VehiclePropertyType::BYTES,
                    .recyclable = false,
                    .vecSize = 5,
            },
            {
                    .type = VehiclePropertyType::STRING,
                    .recyclable = false,
                    .vecSize = 0,
            },
            {
                    .type = VehiclePropertyType::MIXED,
                    .recyclable = false,
                    .vecSize = 0,
            },
    };
}

}  // namespace

class VehicleObjectPoolTest : public ::testing::Test {
  protected:
    void SetUp() override {
        mStats = PoolStats::instance();
        resetStats();
        mValuePool.reset(new VehiclePropValuePool);
    }

    void TearDown() override {
        // At the end, all created objects should be either recycled or deleted.
        ASSERT_EQ(mStats->Obtained, mStats->Recycled + mStats->Deleted);
        // Some objects could be recycled multiple times.
        ASSERT_LE(mStats->Created, mStats->Recycled + mStats->Deleted);
    }

    PoolStats* mStats;
    std::unique_ptr<VehiclePropValuePool> mValuePool;

  private:
    void resetStats() {
        mStats->Obtained = 0;
        mStats->Created = 0;
        mStats->Recycled = 0;
        mStats->Deleted = 0;
    }
};

class VehiclePropertyTypesTest : public VehicleObjectPoolTest,
                                 public testing::WithParamInterface<TestPropertyTypeInfo> {};

TEST_P(VehiclePropertyTypesTest, testRecycle) {
    auto info = GetParam();
    if (!info.recyclable) {
        GTEST_SKIP();
    }

    auto value = mValuePool->obtain(info.type, info.vecSize);
    void* raw = value.get();
    value.reset();
    // At this point, value should be recycled and the only object in the pool.
    ASSERT_EQ(mValuePool->obtain(info.type, info.vecSize).get(), raw);

    ASSERT_EQ(mStats->Obtained, 2u);
    ASSERT_EQ(mStats->Created, 1u);
}

TEST_P(VehiclePropertyTypesTest, testNotRecyclable) {
    auto info = GetParam();
    if (info.recyclable) {
        GTEST_SKIP();
    }

    auto value = mValuePool->obtain(info.type, info.vecSize);

    ASSERT_EQ(mStats->Obtained, 0u) << "Non recyclable object should not be obtained from the pool";
    ASSERT_EQ(mStats->Created, 0u) << "Non recyclable object should not be created from the pool";
}

INSTANTIATE_TEST_SUITE_P(AllPropertyTypes, VehiclePropertyTypesTest,
                         ::testing::ValuesIn(getAllPropertyTypes()));

TEST_F(VehicleObjectPoolTest, testObtainNewObject) {
    auto value = mValuePool->obtain(VehiclePropertyType::INT32);
    void* raw = value.get();
    value.reset();
    // At this point, value should be recycled and the only object in the pool.
    ASSERT_EQ(mValuePool->obtain(VehiclePropertyType::INT32).get(), raw);
    // Obtaining value of another type - should return a new object
    ASSERT_NE(mValuePool->obtain(VehiclePropertyType::FLOAT).get(), raw);

    ASSERT_EQ(mStats->Obtained, 3u);
    ASSERT_EQ(mStats->Created, 2u);
}

TEST_F(VehicleObjectPoolTest, testObtainStrings) {
    mValuePool->obtain(VehiclePropertyType::STRING);
    auto stringProp = mValuePool->obtain(VehiclePropertyType::STRING);
    stringProp->value.stringValue = "Hello";
    void* raw = stringProp.get();
    stringProp.reset();  // delete the pointer

    auto newStringProp = mValuePool->obtain(VehiclePropertyType::STRING);

    ASSERT_EQ(newStringProp->value.stringValue.size(), 0u);
    ASSERT_NE(mValuePool->obtain(VehiclePropertyType::STRING).get(), raw);
    ASSERT_EQ(mStats->Obtained, 0u);
}

TEST_F(VehicleObjectPoolTest, testObtainBoolean) {
    auto prop = mValuePool->obtainBoolean(true);

    ASSERT_NE(prop, nullptr);
    ASSERT_EQ(*prop, (VehiclePropValue{
                             .value = {.int32Values = {1}},
                     }));
}

TEST_F(VehicleObjectPoolTest, testObtainInt32) {
    auto prop = mValuePool->obtainInt32(1234);

    ASSERT_NE(prop, nullptr);
    ASSERT_EQ(*prop, (VehiclePropValue{
                             .value = {.int32Values = {1234}},
                     }));
}

TEST_F(VehicleObjectPoolTest, testObtainInt64) {
    auto prop = mValuePool->obtainInt64(1234);

    ASSERT_NE(prop, nullptr);
    ASSERT_EQ(*prop, (VehiclePropValue{
                             .value = {.int64Values = {1234}},
                     }));
}

TEST_F(VehicleObjectPoolTest, testObtainFloat) {
    auto prop = mValuePool->obtainFloat(1.234);

    ASSERT_NE(prop, nullptr);
    ASSERT_EQ(*prop, (VehiclePropValue{
                             .value = {.floatValues = {1.234}},
                     }));
}

TEST_F(VehicleObjectPoolTest, testObtainString) {
    auto prop = mValuePool->obtainString("test");

    ASSERT_NE(prop, nullptr);
    ASSERT_EQ(*prop, (VehiclePropValue{
                             .value = {.stringValue = "test"},
                     }));
}

TEST_F(VehicleObjectPoolTest, testObtainComplex) {
    auto prop = mValuePool->obtainComplex();

    ASSERT_NE(prop, nullptr);
    ASSERT_EQ(*prop, VehiclePropValue{});
}

TEST_F(VehicleObjectPoolTest, testObtainCopyInt32Values) {
    VehiclePropValue prop{
            // INT32_VEC property.
            .prop = toInt(VehicleProperty::INFO_FUEL_TYPE),
            .areaId = 2,
            .timestamp = 3,
            .value = {.int32Values = {1, 2, 3, 4}},
    };
    auto gotValue = mValuePool->obtain(prop);

    ASSERT_NE(gotValue, nullptr);
    ASSERT_EQ(*gotValue, prop);
}

TEST_F(VehicleObjectPoolTest, testObtainCopyInt64Values) {
    VehiclePropValue prop{
            // INT64_VEC property.
            .prop = toInt(VehicleProperty::WHEEL_TICK),
            .areaId = 2,
            .timestamp = 3,
            .value = {.int64Values = {1, 2, 3, 4}},
    };
    auto gotValue = mValuePool->obtain(prop);

    ASSERT_NE(gotValue, nullptr);
    ASSERT_EQ(*gotValue, prop);
}

TEST_F(VehicleObjectPoolTest, testObtainCopyFloatValues) {
    VehiclePropValue prop{
            // FLOAT_VEC property.
            .prop = toInt(VehicleProperty::HVAC_TEMPERATURE_VALUE_SUGGESTION),
            .areaId = 2,
            .timestamp = 3,
            .value = {.floatValues = {1, 2, 3, 4}},
    };
    auto gotValue = mValuePool->obtain(prop);

    ASSERT_NE(gotValue, nullptr);
    ASSERT_EQ(*gotValue, prop);
}

TEST_F(VehicleObjectPoolTest, testObtainCopyString) {
    VehiclePropValue prop{
            // STRING property.
            .prop = toInt(VehicleProperty::INFO_VIN),
            .areaId = 2,
            .timestamp = 3,
            .value = {.stringValue = "test"},
    };
    auto gotValue = mValuePool->obtain(prop);

    ASSERT_NE(gotValue, nullptr);
    ASSERT_EQ(*gotValue, prop);
}

TEST_F(VehicleObjectPoolTest, testObtainCopyMixed) {
    VehiclePropValue prop{
            // MIxed property.
            .prop = toInt(VehicleProperty::VEHICLE_MAP_SERVICE),
            .areaId = 2,
            .timestamp = 3,
            .value =
                    {
                            .int32Values = {1, 2, 3},
                            .floatValues = {4.0, 5.0},
                            .stringValue = "test",
                    },
    };
    auto gotValue = mValuePool->obtain(prop);

    ASSERT_NE(gotValue, nullptr);
    ASSERT_EQ(*gotValue, prop);
}

TEST_F(VehicleObjectPoolTest, testMultithreaded) {
    // In this test we have T threads that concurrently in C cycles
    // obtain and release O VehiclePropValue objects of FLOAT / INT32 types.

    const int T = 2;
    const int C = 500;
    const int O = 100;

    auto poolPtr = mValuePool.get();

    std::vector<std::thread> threads;
    for (int i = 0; i < T; i++) {
        threads.push_back(std::thread([&poolPtr]() {
            for (int j = 0; j < C; j++) {
                std::vector<recyclable_ptr<VehiclePropValue>> vec;
                for (int k = 0; k < O; k++) {
                    vec.push_back(poolPtr->obtain(k % 2 == 0 ? VehiclePropertyType::FLOAT
                                                             : VehiclePropertyType::INT32));
                }
            }
        }));
    }

    for (auto& t : threads) {
        t.join();
    }

    ASSERT_EQ(mStats->Obtained, static_cast<uint32_t>(T * C * O));
    ASSERT_EQ(mStats->Recycled + mStats->Deleted, static_cast<uint32_t>(T * C * O));
    // Created less than obtained in one cycle.
    ASSERT_LE(mStats->Created, static_cast<uint32_t>(T * O));
}

TEST_F(VehicleObjectPoolTest, testMemoryLimitation) {
    std::vector<recyclable_ptr<VehiclePropValue>> vec;
    for (size_t i = 0; i < 10000; i++) {
        vec.push_back(mValuePool->obtain(VehiclePropertyType::INT32));
    }
    // We have too many values, not all of them would be recycled, some of them will be deleted.
    vec.clear();

    ASSERT_EQ(mStats->Obtained, 10000u);
    ASSERT_EQ(mStats->Created, 10000u);
    ASSERT_GT(mStats->Deleted, 0u) << "expect some values to be deleted, not recycled if too many "
                                      "values are in the pool";
}

}  // namespace vehicle
}  // namespace automotive
}  // namespace hardware
}  // namespace android
