#include "bej_dictionary.h"
#include "bej_encoder_core.h"
#include "bej_tree.h"

#include "bej_common_test.hpp"
#include "bej_decoder_json.hpp"
#include "bej_encoder_json.hpp"

#include <vector>

#include <gmock/gmock-matchers.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

namespace libbej
{

struct BejEncoderTestParams
{
    const std::string testName;
    const BejTestInputFiles inputFiles;
    std::string expectedJson;
    struct RedfishPropertyParent* (*createResource)();
};

void PrintTo(const BejEncoderTestParams& params, std::ostream* os)
{
    *os << params.testName;
}

using BejEncoderTest = testing::TestWithParam<BejEncoderTestParams>;

const BejTestInputFiles dummySimpleTestFiles = {
    .jsonFile = "../test/json/dummysimple.json",
    .schemaDictionaryFile = "../test/dictionaries/dummy_simple_dict.bin",
    .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
    .errorDictionaryFile = "",
    .encodedStreamFile = "../test/encoded/dummy_simple_enc.bin",
};

const BejTestInputFiles driveOemTestFiles = {
    .jsonFile = "../test/json/drive_oem.json",
    .schemaDictionaryFile = "../test/dictionaries/drive_oem_dict.bin",
    .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
    .errorDictionaryFile = "",
    .encodedStreamFile = "../test/encoded/drive_oem_enc.bin",
};

const BejTestInputFiles chassisTestFiles = {
    .jsonFile = "../test/json/chassis.json",
    .schemaDictionaryFile = "../test/dictionaries/chassis_dict.bin",
    .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
    .errorDictionaryFile = "",
    .encodedStreamFile = "../test/encoded/chassis_enc.bin",
};

struct RedfishPropertyParent* createDummyResource()
{
    static struct RedfishPropertyParent root;
    bejTreeInitSet(&root, "DummySimple");

    static struct RedfishPropertyLeafString id;
    bejTreeAddString(&root, &id, "Id", "Dummy ID");

    static struct RedfishPropertyLeafInt intProp;
    bejTreeAddInteger(&root, &intProp, "SampleIntegerProperty", -5);

    static struct RedfishPropertyLeafReal real;
    bejTreeAddReal(&root, &real, "SampleRealProperty", -5576.90001);

    static struct RedfishPropertyLeafNull enProp;
    bejTreeAddNull(&root, &enProp, "SampleEnabledProperty");

    static struct RedfishPropertyParent chArraySet1;
    bejTreeInitSet(&chArraySet1, nullptr);

    static struct RedfishPropertyLeafBool chArraySet1bool;
    bejTreeAddBool(&chArraySet1, &chArraySet1bool, "AnotherBoolean", true);

    static struct RedfishPropertyLeafEnum chArraySet1Ls;
    bejTreeAddEnum(&chArraySet1, &chArraySet1Ls, "LinkStatus", "NoLink");

    static struct RedfishPropertyParent chArraySet2;
    bejTreeInitSet(&chArraySet2, nullptr);

    static struct RedfishPropertyLeafEnum chArraySet2Ls;
    bejTreeAddEnum(&chArraySet2, &chArraySet2Ls, "LinkStatus", "LinkDown");

    static struct RedfishPropertyParent chArray;
    bejTreeInitArray(&chArray, "ChildArrayProperty");

    bejTreeLinkChildToParent(&chArray, &chArraySet1);
    bejTreeLinkChildToParent(&chArray, &chArraySet2);

    bejTreeLinkChildToParent(&root, &chArray);
    return &root;
}

const std::string driveOemJson = R"(
      {
        "@odata.id": "/redfish/v1/drives/1",
        "@odata.type": "#Drive.v1_5_0.Drive",
        "Id": "Drive1",
        "Actions": {
            "#Drive.Reset": {
                "target": "/redfish/v1/drives/1/Actions/Drive.Reset",
                "title": "Reset a Drive",
                "ResetType@Redfish.AllowableValues": [
                        "On",
                        "ForceOff",
                        "ForceRestart",
                        "Nmi",
                        "ForceOn",
                        "PushPowerButton"
                    ]
            }
        },
        "Status@Message.ExtendedInfo": [
            {
                "MessageId": "PredictiveFailure",
                "RelatedProperties": ["FailurePredicted", "MediaType"]
            }
        ],
        "Identifiers": [],
        "Links": {}
    }
)";

struct RedfishPropertyParent* createDriveOem()
{
    static struct RedfishPropertyParent root;
    bejTreeInitSet(&root, "Drive");

    static struct RedfishPropertyLeafString odataId;
    bejTreeAddString(&root, &odataId, "@odata.id", "/redfish/v1/drives/1");

    static struct RedfishPropertyLeafString odataType;
    bejTreeAddString(&root, &odataType, "@odata.type", "#Drive.v1_5_0.Drive");

    static struct RedfishPropertyLeafString id;
    bejTreeAddString(&root, &id, "Id", "Drive1");

    static struct RedfishPropertyParent actions;
    bejTreeInitSet(&actions, "Actions");

    static struct RedfishPropertyParent drRst;
    bejTreeInitSet(&drRst, "#Drive.Reset");

    static struct RedfishPropertyLeafString drRstTarget;
    bejTreeAddString(&drRst, &drRstTarget, "target",
                     "/redfish/v1/drives/1/Actions/Drive.Reset");

    static struct RedfishPropertyLeafString drRstTitle;
    bejTreeAddString(&drRst, &drRstTitle, "title", "Reset a Drive");

    static struct RedfishPropertyParent drRstType;
    bejTreeInitPropertyAnnotated(&drRstType, "ResetType");

    static struct RedfishPropertyParent drRstTypeAllowable;
    bejTreeInitArray(&drRstTypeAllowable, "@Redfish.AllowableValues");

    static struct RedfishPropertyLeafString drRstTypeAllowableS1;
    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS1, "", "On");

    static struct RedfishPropertyLeafString drRstTypeAllowableS2;
    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS2, "",
                     "ForceOff");

    static struct RedfishPropertyLeafString drRstTypeAllowableS3;
    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS3, "",
                     "ForceRestart");

    static struct RedfishPropertyLeafString drRstTypeAllowableS4;
    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS4, "", "Nmi");

    static struct RedfishPropertyLeafString drRstTypeAllowableS5;
    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS5, "", "ForceOn");

    static struct RedfishPropertyLeafString drRstTypeAllowableS6;
    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS6, "",
                     "PushPowerButton");

    bejTreeLinkChildToParent(&drRstType, &drRstTypeAllowable);
    bejTreeLinkChildToParent(&drRst, &drRstType);
    bejTreeLinkChildToParent(&actions, &drRst);
    bejTreeLinkChildToParent(&root, &actions);

    static struct RedfishPropertyParent statusAnt;
    bejTreeInitPropertyAnnotated(&statusAnt, "Status");

    static struct RedfishPropertyParent statusAntMsgExtInfo;
    bejTreeInitArray(&statusAntMsgExtInfo, "@Message.ExtendedInfo");

    static struct RedfishPropertyParent statusAntMsgExtInfoSet1;
    bejTreeInitSet(&statusAntMsgExtInfoSet1, nullptr);

    static struct RedfishPropertyLeafString statusAntMsgExtInfoSet1P1;
    bejTreeAddString(&statusAntMsgExtInfoSet1, &statusAntMsgExtInfoSet1P1,
                     "MessageId", "PredictiveFailure");

    static struct RedfishPropertyParent statusAntMsgExtInfoSet1P2;
    bejTreeInitArray(&statusAntMsgExtInfoSet1P2, "RelatedProperties");
    bejTreeLinkChildToParent(&statusAntMsgExtInfoSet1,
                             &statusAntMsgExtInfoSet1P2);

    static struct RedfishPropertyLeafString statusAntMsgExtInfoSet1P2Ele1;
    bejTreeAddString(&statusAntMsgExtInfoSet1P2, &statusAntMsgExtInfoSet1P2Ele1,
                     "", "FailurePredicted");

    static struct RedfishPropertyLeafString statusAntMsgExtInfoSet1P2Ele2;
    bejTreeAddString(&statusAntMsgExtInfoSet1P2, &statusAntMsgExtInfoSet1P2Ele2,
                     "", "MediaType");

    bejTreeLinkChildToParent(&statusAntMsgExtInfo, &statusAntMsgExtInfoSet1);
    bejTreeLinkChildToParent(&statusAnt, &statusAntMsgExtInfo);
    bejTreeLinkChildToParent(&root, &statusAnt);

    static struct RedfishPropertyParent identifiers;
    bejTreeInitArray(&identifiers, "Identifiers");
    bejTreeLinkChildToParent(&root, &identifiers);

    static struct RedfishPropertyParent links;
    bejTreeInitSet(&links, "Links");
    bejTreeLinkChildToParent(&root, &links);

    return &root;
}

/**
 * @brief Storage for an array of links with an annotated odata.count.
 *
 * This doesn't contain storage for the link itself.
 *
 * Eg:
 * "Contains": [],
 * "Contains@odata.count": 0,
 */
struct RedfishArrayOfLinksJson
{
    struct RedfishPropertyParent array;
    struct RedfishPropertyParent annotatedProperty;
    struct RedfishPropertyLeafInt count;
};

/**
 * @brief Storage for a single odata.id link inside a JSON "Set" object.
 *
 * Eg: FieldName: {
 *   "@odata.id": "/redfish/v1/Chassis/Something"
 * }
 */
struct RedfishLinkJson
{
    struct RedfishPropertyParent set;
    struct RedfishPropertyLeafString odataId;
};

void addLinkToTree(struct RedfishPropertyParent* parent,
                   struct RedfishPropertyParent* linkSet,
                   const char* linkSetLabel,
                   struct RedfishPropertyLeafString* odataId,
                   const char* linkValue)
{
    bejTreeInitSet(linkSet, linkSetLabel);
    bejTreeAddString(linkSet, odataId, "@odata.id", linkValue);
    bejTreeLinkChildToParent(parent, linkSet);
}

void redfishCreateArrayOfLinksJson(
    struct RedfishPropertyParent* parent, const char* arrayName, int linkCount,
    const char* const links[], struct RedfishArrayOfLinksJson* linksInfo,
    struct RedfishLinkJson* linkJsonArray)
{
    bejTreeInitArray(&linksInfo->array, arrayName);
    bejTreeLinkChildToParent(parent, &linksInfo->array);

    bejTreeInitPropertyAnnotated(&linksInfo->annotatedProperty, arrayName);
    bejTreeAddInteger(&linksInfo->annotatedProperty, &linksInfo->count,
                      "@odata.count", linkCount);
    bejTreeLinkChildToParent(parent, &linksInfo->annotatedProperty);

    for (int i = 0; i < linkCount; ++i)
    {
        addLinkToTree(&linksInfo->array, &linkJsonArray[i].set, nullptr,
                      &linkJsonArray[i].odataId, links[i]);
    }
}

struct RedfishPropertyParent* createChassisResource()
{
    constexpr int containsLinkCount = 2;
    const char* contains[containsLinkCount] = {"/redfish/v1/Chassis/Disk_0",
                                               "/redfish/v1/Chassis/Disk_1"};
    const char* storage[1] = {"/redfish/v1/Systems/system/Storage/SATA"};
    const char* drives[1] = {"/redfish/v1/Chassis/SomeChassis/Drives/SATA_0"};
    static struct RedfishPropertyParent root;
    static struct RedfishPropertyLeafString odataId;
    static struct RedfishPropertyParent links;
    static struct RedfishPropertyParent computerSystemsArray;
    static struct RedfishPropertyParent computerSystemsLinkSet;
    static struct RedfishPropertyLeafString computerSystemsLinkOdataId;
    static struct RedfishPropertyParent containedBySet;
    static struct RedfishPropertyLeafString containedByOdataId;
    static struct RedfishArrayOfLinksJson containsArray;
    static struct RedfishLinkJson containsLinks[containsLinkCount];
    static struct RedfishArrayOfLinksJson storageArray;
    static struct RedfishLinkJson storageLink;
    static struct RedfishArrayOfLinksJson drives_array;
    static struct RedfishLinkJson drive_link;

    bejTreeInitSet(&root, "Chassis");
    bejTreeAddString(&root, &odataId, "@odata.id",
                     "/redfish/v1/Chassis/SomeChassis");

    bejTreeInitSet(&links, "Links");
    bejTreeLinkChildToParent(&root, &links);

    bejTreeInitArray(&computerSystemsArray, "ComputerSystems");
    bejTreeLinkChildToParent(&links, &computerSystemsArray);

    addLinkToTree(&computerSystemsArray, &computerSystemsLinkSet, "",
                  &computerSystemsLinkOdataId, "/redfish/v1/Systems/system");

    addLinkToTree(&links, &containedBySet, "ContainedBy", &containedByOdataId,
                  "/redfish/v1/Chassis/SomeOtherChassis");

    redfishCreateArrayOfLinksJson(&links, "Contains", containsLinkCount,
                                  contains, &containsArray, containsLinks);

    redfishCreateArrayOfLinksJson(&links, "Storage", /*linkCount=*/1, storage,
                                  &storageArray, &storageLink);

    redfishCreateArrayOfLinksJson(&links, "Drives", /*linkCount=*/1, drives,
                                  &drives_array, &drive_link);

    return &root;
}

TEST_P(BejEncoderTest, Encode)
{
    const BejEncoderTestParams& test_case = GetParam();
    auto inputsOrErr = loadInputs(test_case.inputFiles);
    EXPECT_TRUE(inputsOrErr);

    BejDictionaries dictionaries = {
        .schemaDictionary = inputsOrErr->schemaDictionary,
        .annotationDictionary = inputsOrErr->annotationDictionary,
        .errorDictionary = inputsOrErr->errorDictionary,
    };

    std::vector<uint8_t> outputBuffer;
    struct BejEncoderOutputHandler output = {
        .handlerContext = &outputBuffer,
        .recvOutput = &getBejEncodedBuffer,
    };

    std::vector<void*> pointerStack;
    struct BejPointerStackCallback stackCallbacks = {
        .stackContext = &pointerStack,
        .stackEmpty = stackEmpty,
        .stackPeek = stackPeek,
        .stackPop = stackPop,
        .stackPush = stackPush,
        .deleteStack = nullptr,
    };

    bejEncode(&dictionaries, BEJ_DICTIONARY_START_AT_HEAD, bejMajorSchemaClass,
              test_case.createResource(), &output, &stackCallbacks);

    BejDecoderJson decoder;
    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)), 0);
    std::string decoded = decoder.getOutput();
    nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);

    if (!test_case.expectedJson.empty())
    {
        inputsOrErr->expectedJson =
            nlohmann::json::parse(test_case.expectedJson);
    }
    EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
}

TEST_P(BejEncoderTest, EncodeWrapper)
{
    const BejEncoderTestParams& test_case = GetParam();
    auto inputsOrErr = loadInputs(test_case.inputFiles);
    EXPECT_TRUE(inputsOrErr);

    BejDictionaries dictionaries = {
        .schemaDictionary = inputsOrErr->schemaDictionary,
        .annotationDictionary = inputsOrErr->annotationDictionary,
        .errorDictionary = inputsOrErr->errorDictionary,
    };

    libbej::BejEncoderJson encoder;
    encoder.encode(&dictionaries, bejMajorSchemaClass,
                   test_case.createResource());

    std::vector<uint8_t> outputBuffer = encoder.getOutput();
    BejDecoderJson decoder;
    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)), 0);
    std::string decoded = decoder.getOutput();
    nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);

    if (!test_case.expectedJson.empty())
    {
        inputsOrErr->expectedJson =
            nlohmann::json::parse(test_case.expectedJson);
    }
    EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());

    // Try using the same encoder object again to ensure that the same object
    // does the encoding correctly
    encoder.encode(&dictionaries, bejMajorSchemaClass,
                   test_case.createResource());

    outputBuffer = encoder.getOutput();

    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)), 0);
    decoded = decoder.getOutput();
    jsonDecoded = nlohmann::json::parse(decoded);

    if (!test_case.expectedJson.empty())
    {
        inputsOrErr->expectedJson =
            nlohmann::json::parse(test_case.expectedJson);
    }
    EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
}

/**
 * TODO: Add more test cases.
 */

INSTANTIATE_TEST_SUITE_P(
    , BejEncoderTest,
    testing::ValuesIn<BejEncoderTestParams>({
        {"DriveOEM", driveOemTestFiles, driveOemJson, &createDriveOem},
        {"DummySimple", dummySimpleTestFiles, "", &createDummyResource},
        {"Chassis", chassisTestFiles, "", &createChassisResource},
    }),
    [](const testing::TestParamInfo<BejEncoderTest::ParamType>& info) {
        return info.param.testName;
    });

} // namespace libbej