/*
 * Copyright (C) 2017-2018 Intel Corporation
 *
 * SPDX-License-Identifier: MIT
 *
 */

#include <runtime/helpers/hash.h>
#include <runtime/helpers/hw_info.h>
#include <runtime/compiler_interface/binary_cache.h>
#include "runtime/compiler_interface/compiler_interface.h"
#include <runtime/helpers/string.h>
#include <runtime/helpers/aligned_memory.h>
#include <unit_tests/global_environment.h>
#include <unit_tests/fixtures/device_fixture.h>
#include <unit_tests/mocks/mock_context.h>
#include <unit_tests/mocks/mock_program.h>

#include <memory>
#include <array>
#include <list>

#include "test.h"

using namespace OCLRT;
using namespace std;

class BinaryCacheFixture

{
  public:
    void SetUp() {
        cache = new BinaryCache;
    }

    void TearDown() {
        delete cache;
    }
    BinaryCache *cache;
};

class BinaryCacheMock : public BinaryCache {
  public:
    BinaryCacheMock() {
    }

    bool cacheBinary(const std::string kernelFileHash, const char *pBinary, uint32_t binarySize) override {
        cacheInvoked++;
        return cacheResult;
    }

    bool loadCachedBinary(const std::string kernelFileHash, Program &program) override {
        return loadResult;
    }

    bool cacheResult = false;
    uint32_t cacheInvoked = 0u;
    bool loadResult = false;
};

class CompilerInterfaceCachedFixture : public DeviceFixture {
  public:
    void SetUp() {
        DeviceFixture::SetUp();
        pCompilerInterface = pDevice->getExecutionEnvironment()->getCompilerInterface();
        ASSERT_NE(pCompilerInterface, nullptr);
    }

    void TearDown() {
        DeviceFixture::TearDown();
    }

    CompilerInterface *pCompilerInterface;
};

typedef Test<BinaryCacheFixture> BinaryCacheHashTests;
typedef Test<BinaryCacheFixture> BinaryCacheTests;
typedef Test<CompilerInterfaceCachedFixture> CompilerInterfaceCachedTests;

TEST(HashGeneration, givenMisalignedBufferWhenPassedToUpdateFunctionThenProperPtrDataIsUsed) {
    Hash hash;
    auto originalPtr = alignedMalloc(1024, MemoryConstants::pageSize);

    memset(originalPtr, 0xFF, 1024);
    char *misalignedPtr = (char *)originalPtr;
    misalignedPtr++;

    //values really used
    misalignedPtr[0] = 1;
    misalignedPtr[1] = 2;
    misalignedPtr[2] = 3;
    misalignedPtr[3] = 4;
    misalignedPtr[4] = 5;
    //values not used should be ommitted
    misalignedPtr[5] = 6;
    misalignedPtr[6] = 7;

    hash.update(misalignedPtr, 3);
    auto hash1 = hash.finish();

    hash.reset();
    hash.update(misalignedPtr, 4);
    auto hash2 = hash.finish();

    hash.reset();
    hash.update(misalignedPtr, 5);
    auto hash3 = hash.finish();

    hash.reset();
    hash.update(misalignedPtr, 6);
    auto hash4 = hash.finish();

    EXPECT_NE(hash1, hash2);
    EXPECT_NE(hash1, hash3);
    EXPECT_NE(hash1, hash4);
    EXPECT_NE(hash2, hash3);
    EXPECT_NE(hash2, hash4);
    EXPECT_NE(hash3, hash4);

    auto value2 = hash.getValue(misalignedPtr, 0);
    EXPECT_EQ(0u, value2);

    alignedFree(originalPtr);
}

TEST(HashGeneration, givenMisalignedBufferWithSizeOneWhenPassedToUpdateFunctionThenProperPtrDataIsUsed) {
    Hash hash;
    auto originalPtr = alignedMalloc(1024, MemoryConstants::pageSize);

    memset(originalPtr, 0xFF, 1024);
    char *misalignedPtr = (char *)originalPtr;
    misalignedPtr++;

    //values really used
    misalignedPtr[0] = 1;
    //values not used should be ommitted
    misalignedPtr[1] = 2;
    misalignedPtr[2] = 3;
    misalignedPtr[3] = 4;
    misalignedPtr[4] = 5;
    misalignedPtr[5] = 6;
    misalignedPtr[6] = 7;

    hash.update(misalignedPtr, 1);

    auto value = hash.finish();
    EXPECT_EQ(0x088350e6600f29c2u, value);

    alignedFree(originalPtr);
}

TEST_F(BinaryCacheHashTests, hashShortBuffers) {
    Hash hash;

    std::list<uint64_t> hashes;
    char data[4] = "aBc";

    for (size_t i = 0; i <= strlen(data); i++) {
        hash.reset();
        hash.update(data, i);
        auto res = hash.finish();

        for (auto &in : hashes) {
            EXPECT_NE(in, res) << "failed: " << i << " bytes";
        }
        hashes.push_back(res);

        // hash once again to make sure results are the same
        hash.reset();
        hash.update(data, i);
        auto res2 = hash.finish();
        EXPECT_EQ(res, res2);
    }
}

TEST_F(BinaryCacheHashTests, testUnique) {
    static const size_t bufSize = 64;
    TranslationArgs args;
    HardwareInfo hwInfo;

    std::set<std::string> hashes;

    PLATFORM p1 = {(PRODUCT_FAMILY)1};
    PLATFORM p2 = {(PRODUCT_FAMILY)2};
    const PLATFORM *platforms[] = {nullptr, &p1, &p2};
    FeatureTable s1;
    FeatureTable s2;
    s1.ftrSVM = true;
    s2.ftrSVM = false;
    const FeatureTable *skus[] = {nullptr, &s1, &s2};
    WorkaroundTable w1;
    WorkaroundTable w2;
    w1.waDoNotUseMIReportPerfCount = true;
    w2.waDoNotUseMIReportPerfCount = false;
    const WorkaroundTable *was[] = {nullptr, &w1, &w2};
    // GT_SYSTEM_INFO s1 = {};

    std::array<std::string, 4> input = {{std::string(""),
                                         std::string("12345678901234567890123456789012"),
                                         std::string("12345678910234567890123456789012"),
                                         std::string("12345678901234567891023456789012")}};

    std::array<std::string, 3> options = {{std::string(""),
                                           std::string("--some --options"),
                                           std::string("--some --different --options")}};

    std::array<std::string, 3> internalOptions = {{std::string(""),
                                                   std::string("--some --options"),
                                                   std::string("--some --different --options")}};

    std::array<std::string, 1> tracingOptions = {{
        std::string(""),
        //        std::string("--some --options"),
        //        std::string("--some --different --options")
    }};

    std::unique_ptr<char> buf1(new char[bufSize]);
    std::unique_ptr<char> buf2(new char[bufSize]);
    std::unique_ptr<char> buf3(new char[bufSize]);
    std::unique_ptr<char> buf4(new char[bufSize]);

    for (auto platform : platforms) {
        hwInfo.pPlatform = platform;

        for (auto sku : skus) {
            hwInfo.pSkuTable = sku;

            for (auto wa : was) {
                hwInfo.pWaTable = wa;

                for (size_t i1 = 0; i1 < input.size(); i1++) {
                    strcpy_s(buf1.get(), bufSize, input[i1].c_str());
                    args.pInput = buf1.get();
                    args.InputSize = static_cast<uint32_t>(strlen(buf1.get()));
                    for (size_t i2 = 0; i2 < options.size(); i2++) {
                        strcpy_s(buf2.get(), bufSize, options[i2].c_str());
                        args.pOptions = buf2.get();
                        args.OptionsSize = static_cast<uint32_t>(strlen(buf2.get()));
                        for (size_t i3 = 0; i3 < internalOptions.size(); i3++) {
                            strcpy_s(buf3.get(), bufSize, internalOptions[i3].c_str());
                            args.pInternalOptions = buf3.get();
                            args.InternalOptionsSize = static_cast<uint32_t>(strlen(buf3.get()));
                            for (size_t i4 = 0; i4 < tracingOptions.size(); i4++) {
                                strcpy_s(buf4.get(), bufSize, tracingOptions[i4].c_str());
                                args.pTracingOptions = buf4.get();
                                args.TracingOptionsCount = static_cast<uint32_t>(strlen(buf4.get()));

                                string hash = cache->getCachedFileName(hwInfo, ArrayRef<const char>(args.pInput, args.InputSize),
                                                                       ArrayRef<const char>(args.pOptions, args.OptionsSize),
                                                                       ArrayRef<const char>(args.pInternalOptions, args.InternalOptionsSize));

                                if (hashes.find(hash) != hashes.end()) {
                                    FAIL() << "failed: " << i1 << ":" << i2 << ":" << i3 << ":" << i4;
                                }
                                hashes.emplace(hash);
                            }
                        }
                    }
                }
            }
        }
    }

    string hash = cache->getCachedFileName(hwInfo, ArrayRef<const char>(args.pInput, args.InputSize),
                                           ArrayRef<const char>(args.pOptions, args.OptionsSize),
                                           ArrayRef<const char>(args.pInternalOptions, args.InternalOptionsSize));

    string hash2 = cache->getCachedFileName(hwInfo, ArrayRef<const char>(args.pInput, args.InputSize),
                                            ArrayRef<const char>(args.pOptions, args.OptionsSize),
                                            ArrayRef<const char>(args.pInternalOptions, args.InternalOptionsSize));
    EXPECT_STREQ(hash.c_str(), hash2.c_str());
}

TEST_F(BinaryCacheTests, doNotCacheEmpty) {
    bool ret = cache->cacheBinary("some_hash", nullptr, 12u);
    EXPECT_FALSE(ret);

    const char *tmp1 = "Data";
    ret = cache->cacheBinary("some_hash", tmp1, 0u);
    EXPECT_FALSE(ret);
}

TEST_F(BinaryCacheTests, loadNotFound) {
    ExecutionEnvironment executionEnvironment;
    MockProgram program(executionEnvironment);
    bool ret = cache->loadCachedBinary("----do-not-exists----", program);
    EXPECT_FALSE(ret);
}

TEST_F(BinaryCacheTests, cacheThenLoad) {
    ExecutionEnvironment executionEnvironment;
    MockProgram program(executionEnvironment);
    static const char *hash = "SOME_HASH";
    std::unique_ptr<char> data(new char[32]);
    for (size_t i = 0; i < 32; i++)
        data.get()[i] = static_cast<char>(i);

    bool ret = cache->cacheBinary(hash, static_cast<const char *>(data.get()), 32);
    EXPECT_TRUE(ret);

    ret = cache->loadCachedBinary(hash, program);
    EXPECT_TRUE(ret);
}

TEST_F(CompilerInterfaceCachedTests, canInjectCache) {
    std::unique_ptr<BinaryCache> cache(new BinaryCache());
    auto res1 = pCompilerInterface->replaceBinaryCache(cache.get());
    auto res2 = pCompilerInterface->replaceBinaryCache(res1);

    EXPECT_NE(res1, res2);
    EXPECT_EQ(res2, cache.get());
}
TEST_F(CompilerInterfaceCachedTests, notCachedAndIgcFailed) {
    MockContext context(pDevice, true);
    MockProgram program(*pDevice->getExecutionEnvironment(), &context, false);
    BinaryCacheMock cache;
    TranslationArgs inputArgs;

    inputArgs.pInput = new char[128];
    strcpy_s(inputArgs.pInput, 128, "#include \"header.h\"\n__kernel k() {}");
    inputArgs.InputSize = static_cast<uint32_t>(strlen(inputArgs.pInput));

    MockCompilerDebugVars fclDebugVars;
    fclDebugVars.fileName = gEnvironment->fclGetMockFile();
    gEnvironment->fclPushDebugVars(fclDebugVars);

    MockCompilerDebugVars igcDebugVars;
    igcDebugVars.fileName = gEnvironment->igcGetMockFile();
    igcDebugVars.forceBuildFailure = true;
    gEnvironment->igcPushDebugVars(igcDebugVars);

    auto res1 = pCompilerInterface->replaceBinaryCache(&cache);

    auto retVal = pCompilerInterface->build(program, inputArgs, true);
    EXPECT_NE(CL_SUCCESS, retVal);

    pCompilerInterface->replaceBinaryCache(res1);
    delete[] inputArgs.pInput;

    gEnvironment->fclPopDebugVars();
    gEnvironment->igcPopDebugVars();
}

TEST_F(CompilerInterfaceCachedTests, wasCached) {
    MockContext context(pDevice, true);
    MockProgram program(*pDevice->getExecutionEnvironment(), &context, false);
    BinaryCacheMock cache;
    TranslationArgs inputArgs;

    inputArgs.pInput = new char[128];
    strcpy_s(inputArgs.pInput, 128, "#include \"header.h\"\n__kernel k() {}");
    inputArgs.InputSize = static_cast<uint32_t>(strlen(inputArgs.pInput));

    MockCompilerDebugVars fclDebugVars;
    fclDebugVars.fileName = gEnvironment->fclGetMockFile();
    gEnvironment->fclPushDebugVars(fclDebugVars);

    MockCompilerDebugVars igcDebugVars;
    igcDebugVars.fileName = gEnvironment->igcGetMockFile();
    igcDebugVars.forceBuildFailure = true;
    gEnvironment->igcPushDebugVars(igcDebugVars);

    auto res1 = pCompilerInterface->replaceBinaryCache(&cache);
    cache.loadResult = true;
    auto retVal = pCompilerInterface->build(program, inputArgs, true);
    EXPECT_EQ(CL_SUCCESS, retVal);

    pCompilerInterface->replaceBinaryCache(res1);
    delete[] inputArgs.pInput;

    gEnvironment->fclPopDebugVars();
    gEnvironment->igcPopDebugVars();
}

TEST_F(CompilerInterfaceCachedTests, builtThenCached) {
    MockContext context(pDevice, true);
    MockProgram program(*pDevice->getExecutionEnvironment(), &context, false);
    BinaryCacheMock cache;
    TranslationArgs inputArgs;

    inputArgs.pInput = new char[128];
    strcpy_s(inputArgs.pInput, 128, "#include \"header.h\"\n__kernel k() {}");
    inputArgs.InputSize = static_cast<uint32_t>(strlen(inputArgs.pInput));

    MockCompilerDebugVars fclDebugVars;
    fclDebugVars.fileName = gEnvironment->fclGetMockFile();
    gEnvironment->fclPushDebugVars(fclDebugVars);

    MockCompilerDebugVars igcDebugVars;
    igcDebugVars.fileName = gEnvironment->igcGetMockFile();
    gEnvironment->igcPushDebugVars(igcDebugVars);

    auto res1 = pCompilerInterface->replaceBinaryCache(&cache);
    auto retVal = pCompilerInterface->build(program, inputArgs, true);
    EXPECT_EQ(CL_SUCCESS, retVal);
    EXPECT_EQ(1u, cache.cacheInvoked);

    pCompilerInterface->replaceBinaryCache(res1);
    delete[] inputArgs.pInput;

    gEnvironment->fclPopDebugVars();
    gEnvironment->igcPopDebugVars();
}

TEST_F(CompilerInterfaceCachedTests, givenKernelWithoutIncludesAndBinaryInCacheWhenCompilationRequestedThenFCLIsNotCalled) {
    MockContext context(pDevice, true);
    MockProgram program(*pDevice->getExecutionEnvironment(), &context, false);
    BinaryCacheMock cache;
    TranslationArgs inputArgs;

    inputArgs.pInput = new char[128];
    strcpy_s(inputArgs.pInput, 128, "__kernel k() {}");
    inputArgs.InputSize = static_cast<uint32_t>(strlen(inputArgs.pInput));

    // we force both compilers to fail compilation request
    // at the end we expect CL_SUCCESS which means compilation ends in cache
    MockCompilerDebugVars fclDebugVars;
    fclDebugVars.fileName = gEnvironment->fclGetMockFile();
    fclDebugVars.forceBuildFailure = true;
    gEnvironment->fclPushDebugVars(fclDebugVars);

    MockCompilerDebugVars igcDebugVars;
    igcDebugVars.fileName = gEnvironment->igcGetMockFile();
    igcDebugVars.forceBuildFailure = true;
    gEnvironment->igcPushDebugVars(igcDebugVars);

    auto res = pCompilerInterface->replaceBinaryCache(&cache);
    cache.loadResult = true;
    auto retVal = pCompilerInterface->build(program, inputArgs, true);
    EXPECT_EQ(CL_SUCCESS, retVal);

    pCompilerInterface->replaceBinaryCache(res);
    delete[] inputArgs.pInput;

    gEnvironment->fclPopDebugVars();
    gEnvironment->igcPopDebugVars();
}

TEST_F(CompilerInterfaceCachedTests, givenKernelWithIncludesAndBinaryInCacheWhenCompilationRequestedThenFCLIsCalled) {
    MockContext context(pDevice, true);
    MockProgram program(*pDevice->getExecutionEnvironment(), &context, false);
    BinaryCacheMock cache;
    TranslationArgs inputArgs;

    inputArgs.pInput = new char[128];
    strcpy_s(inputArgs.pInput, 128, "#include \"file.h\"\n__kernel k() {}");
    inputArgs.InputSize = static_cast<uint32_t>(strlen(inputArgs.pInput));

    MockCompilerDebugVars fclDebugVars;
    fclDebugVars.fileName = gEnvironment->fclGetMockFile();
    fclDebugVars.forceBuildFailure = true;
    gEnvironment->fclPushDebugVars(fclDebugVars);

    auto res = pCompilerInterface->replaceBinaryCache(&cache);
    cache.loadResult = true;
    auto retVal = pCompilerInterface->build(program, inputArgs, true);
    EXPECT_EQ(CL_BUILD_PROGRAM_FAILURE, retVal);

    pCompilerInterface->replaceBinaryCache(res);
    delete[] inputArgs.pInput;

    gEnvironment->fclPopDebugVars();
}
