//     __ _____ _____ _____
//  __|  |   __|     |   | |  JSON for Modern C++ (supporting code)
// |  |  |__   |  |  | | | |  version 3.11.3
// |_____|_____|_____|_|___|  https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT

#include <set>
#include <sstream>
#include <string>

#include "doctest_compatibility.h"

#include <nlohmann/json.hpp>

// Test extending nlohmann::json by using a custom base class.
// Add some metadata to each node and test the behaviour of copy / move
template<class MetaDataType>
class json_metadata
{
  public:
    using metadata_t = MetaDataType;
    metadata_t& metadata()
    {
        return m_metadata;
    }
    const metadata_t& metadata() const
    {
        return m_metadata;
    }
  private:
    metadata_t m_metadata = {};
};

template<class T>
using json_with_metadata =
    nlohmann::basic_json <
    std::map,
    std::vector,
    std::string,
    bool,
    std::int64_t,
    std::uint64_t,
    double,
    std::allocator,
    nlohmann::adl_serializer,
    std::vector<std::uint8_t>,
    json_metadata<T>
    >;

TEST_CASE("JSON Node Metadata")
{
    SECTION("type int")
    {
        using json = json_with_metadata<int>;
        json null;
        auto obj   = json::object();
        auto array = json::array();

        null.metadata()  = 1;
        obj.metadata()   = 2;
        array.metadata() = 3;
        auto copy = array;

        CHECK(null.metadata()  == 1);
        CHECK(obj.metadata()   == 2);
        CHECK(array.metadata() == 3);
        CHECK(copy.metadata()  == 3);
    }
    SECTION("type vector<int>")
    {
        using json = json_with_metadata<std::vector<int>>;
        json value;
        value.metadata().emplace_back(1);
        auto copy = value;
        value.metadata().emplace_back(2);

        CHECK(copy.metadata().size()  == 1);
        CHECK(copy.metadata().at(0)   == 1);
        CHECK(value.metadata().size() == 2);
        CHECK(value.metadata().at(0)  == 1);
        CHECK(value.metadata().at(1)  == 2);
    }
    SECTION("copy ctor")
    {
        using json = json_with_metadata<std::vector<int>>;
        json value;
        value.metadata().emplace_back(1);
        value.metadata().emplace_back(2);

        json copy = value;

        CHECK(copy.metadata().size()  == 2);
        CHECK(copy.metadata().at(0)   == 1);
        CHECK(copy.metadata().at(1)   == 2);
        CHECK(value.metadata().size() == 2);
        CHECK(value.metadata().at(0)  == 1);
        CHECK(value.metadata().at(1)  == 2);

        value.metadata().clear();
        CHECK(copy.metadata().size()  == 2);
        CHECK(value.metadata().size() == 0);
    }
    SECTION("move ctor")
    {
        using json = json_with_metadata<std::vector<int>>;
        json value;
        value.metadata().emplace_back(1);
        value.metadata().emplace_back(2);

        const json moved = std::move(value);

        CHECK(moved.metadata().size()  == 2);
        CHECK(moved.metadata().at(0)   == 1);
        CHECK(moved.metadata().at(1)   == 2);
    }
    SECTION("move assign")
    {
        using json = json_with_metadata<std::vector<int>>;
        json value;
        value.metadata().emplace_back(1);
        value.metadata().emplace_back(2);

        json moved;
        moved = std::move(value);

        CHECK(moved.metadata().size()  == 2);
        CHECK(moved.metadata().at(0)   == 1);
        CHECK(moved.metadata().at(1)   == 2);
    }
    SECTION("copy assign")
    {
        using json = json_with_metadata<std::vector<int>>;
        json value;
        value.metadata().emplace_back(1);
        value.metadata().emplace_back(2);

        json copy;
        copy = value;

        CHECK(copy.metadata().size()  == 2);
        CHECK(copy.metadata().at(0)   == 1);
        CHECK(copy.metadata().at(1)   == 2);
        CHECK(value.metadata().size() == 2);
        CHECK(value.metadata().at(0)  == 1);
        CHECK(value.metadata().at(1)  == 2);

        value.metadata().clear();
        CHECK(copy.metadata().size()  == 2);
        CHECK(value.metadata().size() == 0);
    }
    SECTION("type unique_ptr<int>")
    {
        using json = json_with_metadata<std::unique_ptr<int>>;
        json value;
        value.metadata().reset(new int(42)); // NOLINT(cppcoreguidelines-owning-memory)
        auto moved = std::move(value);

        CHECK(moved.metadata() != nullptr);
        CHECK(*moved.metadata() == 42);
    }
    SECTION("type vector<int> in json array")
    {
        using json = json_with_metadata<std::vector<int>>;
        json value;
        value.metadata().emplace_back(1);
        value.metadata().emplace_back(2);

        json const array(10, value);

        CHECK(value.metadata().size() == 2);
        CHECK(value.metadata().at(0)  == 1);
        CHECK(value.metadata().at(1)  == 2);

        for (const auto& val : array)
        {
            CHECK(val.metadata().size() == 2);
            CHECK(val.metadata().at(0)  == 1);
            CHECK(val.metadata().at(1)  == 2);
        }
    }
}

// Test extending nlohmann::json by using a custom base class.
// Add a custom member function template iterating over the whole json tree.
class visitor_adaptor
{
  public:
    template <class Fnc>
    void visit(const Fnc& fnc) const;
  private:
    template <class Ptr, class Fnc>
    void do_visit(const Ptr& ptr, const Fnc& fnc) const;
};

using json_with_visitor_t = nlohmann::basic_json <
                            std::map,
                            std::vector,
                            std::string,
                            bool,
                            std::int64_t,
                            std::uint64_t,
                            double,
                            std::allocator,
                            nlohmann::adl_serializer,
                            std::vector<std::uint8_t>,
                            visitor_adaptor
                            >;

template <class Fnc>
void visitor_adaptor::visit(const Fnc& fnc) const
{
    do_visit(json_with_visitor_t::json_pointer{}, fnc);
}

template <class Ptr, class Fnc>
void visitor_adaptor::do_visit(const Ptr& ptr, const Fnc& fnc) const
{
    using value_t = nlohmann::detail::value_t;
    const json_with_visitor_t& json = *static_cast<const json_with_visitor_t*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
    switch (json.type())
    {
        case value_t::object:
            for (const auto& entry : json.items())
            {
                entry.value().do_visit(ptr / entry.key(), fnc);
            }
            break;
        case value_t::array:
            for (std::size_t i = 0; i < json.size(); ++i)
            {
                json.at(i).do_visit(ptr / std::to_string(i), fnc);
            }
            break;
        case value_t::discarded:
            break;
        case value_t::null:
        case value_t::string:
        case value_t::boolean:
        case value_t::number_integer:
        case value_t::number_unsigned:
        case value_t::number_float:
        case value_t::binary:
        default:
            fnc(ptr, json);
    }
}

TEST_CASE("JSON Visit Node")
{
    json_with_visitor_t json;
    json["null"];
    json["int"]  = -1;
    json["uint"] = 1U;
    json["float"] = 1.0;
    json["boolean"] = true;
    json["string"] = "string";
    json["array"].push_back(0);
    json["array"].push_back(1);
    json["array"].push_back(json);

    std::set<std::string> expected
    {
        "/null - null - null",
        "/int - number_integer - -1",
        "/uint - number_unsigned - 1",
        "/float - number_float - 1.0",
        "/boolean - boolean - true",
        "/string - string - \"string\"",
        "/array/0 - number_integer - 0",
        "/array/1 - number_integer - 1",

        "/array/2/null - null - null",
        "/array/2/int - number_integer - -1",
        "/array/2/uint - number_unsigned - 1",
        "/array/2/float - number_float - 1.0",
        "/array/2/boolean - boolean - true",
        "/array/2/string - string - \"string\"",
        "/array/2/array/0 - number_integer - 0",
        "/array/2/array/1 - number_integer - 1"
    };

    json.visit(
        [&](const json_with_visitor_t::json_pointer & p,
            const json_with_visitor_t& j)
    {
        std::stringstream str;
        str << p.to_string() << " - " ;
        using value_t = nlohmann::detail::value_t;
        switch (j.type())
        {
            case value_t::object:
                str << "object";
                break;
            case value_t::array:
                str << "array";
                break;
            case value_t::discarded:
                str << "discarded";
                break;
            case value_t::null:
                str << "null";
                break;
            case value_t::string:
                str << "string";
                break;
            case value_t::boolean:
                str << "boolean";
                break;
            case value_t::number_integer:
                str << "number_integer";
                break;
            case value_t::number_unsigned:
                str << "number_unsigned";
                break;
            case value_t::number_float:
                str << "number_float";
                break;
            case value_t::binary:
                str << "binary";
                break;
            default:
                str << "error";
                break;
        }
        str << " - "  << j.dump();
        CHECK(json.at(p) == j);
        INFO(str.str());
        CHECK(expected.count(str.str()) == 1);
        expected.erase(str.str());
    }
    );
    CHECK(expected.empty());
}
