#include "../misc/flagenumclass.h"
#include "../misc/levenshtein.h"
#include "../misc/multiarray.h"

#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
#include "../misc/signingkeys.h"
#include "../misc/verification.h"
#endif

#include "../conversion/stringbuilder.h"
#include "../conversion/stringconversion.h"

#include "../io/misc.h"

#include "../tests/testutils.h"

#include "resources/version.h"

#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>

#include <regex>

using namespace std;
using namespace CppUtilities;
using namespace CppUtilities::Literals;
using namespace CPPUNIT_NS;

// test version check macro
#if CPP_UTILITIES_VERSION_CHECK(5, 2, 1) > CPP_UTILITIES_VERSION_CHECK(6, 0, 0)
#error "Check for major version doesn't work"
#endif
#if CPP_UTILITIES_VERSION_CHECK(5, 2, 2) > CPP_UTILITIES_VERSION_CHECK(5, 3, 0)
#error "Check for minor version doesn't work"
#endif
#if CPP_UTILITIES_VERSION_CHECK(5, 2, 1) > CPP_UTILITIES_VERSION_CHECK(5, 2, 2)
#error "Check for path version doesn't work"
#endif
#if CPP_UTILITIES_VERSION < CPP_UTILITIES_VERSION_CHECK(5, 0, 0)
#error "Library version seems wrongly defined, should be already >= 5.0.0"
#endif

/*!
 * \brief The MiscTests class tests misc functions and classes (mainly of files contained by the misc directory).
 */
class MiscTests : public TestFixture {
    CPPUNIT_TEST_SUITE(MiscTests);
    CPPUNIT_TEST(testMultiArray);
    CPPUNIT_TEST(testLevenshtein);
    CPPUNIT_TEST(testTestUtilities);
#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
    CPPUNIT_TEST(testVerification);
#endif
    CPPUNIT_TEST_SUITE_END();

public:
    void setUp()
    {
    }
    void tearDown()
    {
    }

    void testMultiArray();
    void testLevenshtein();
    void testTestUtilities();
#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
    void testVerification();
#endif
};

CPPUNIT_TEST_SUITE_REGISTRATION(MiscTests);

void MiscTests::testMultiArray()
{
    static_assert(decltype(makeMultiArray<char>(3))::dimensionCount() == 1, "dimension count 1D");
    static_assert(decltype(makeMultiArray<char>(3, 2))::dimensionCount() == 2, "dimension count 2D");
    static_assert(decltype(makeMultiArray<char>(3, 2, 3))::dimensionCount() == 3, "dimension count 3D");

    auto array1d(makeMultiArray<char>(3));
    CPPUNIT_ASSERT_EQUAL(3_st, array1d.dimensionSize<0>());
    CPPUNIT_ASSERT_EQUAL(3_st, array1d.totalSize());
    array1d.at(0) = 'a';
    array1d.at(1) = 'b';
    array1d.at(2) = 'c';
    CPPUNIT_ASSERT_EQUAL("abc"s, string(array1d.data(), 3));

    auto array2d(makeMultiArray<char>(3, 2));
    CPPUNIT_ASSERT_EQUAL(3_st, array2d.dimensionSize<0>());
    CPPUNIT_ASSERT_EQUAL(2_st, array2d.dimensionSize<1>());
    CPPUNIT_ASSERT_EQUAL(6_st, array2d.totalSize());
    const char *const data(array2d.data());
    array2d.at(0, 0) = 'a';
    array2d.at(0, 1) = 'b';
    array2d.at(1, 0) = 'c';
    array2d.at(1, 1) = 'd';
    array2d.at(2, 0) = 'e';
    array2d.at(2, 1) = 'f';
    CPPUNIT_ASSERT_EQUAL("abcdef"s, string(data, 6));

    auto array3d(makeMultiArray<char>(3, 2, 3));
    CPPUNIT_ASSERT_EQUAL(3_st, array3d.dimensionSize<0>());
    CPPUNIT_ASSERT_EQUAL(2_st, array3d.dimensionSize<1>());
    CPPUNIT_ASSERT_EQUAL(3_st, array3d.dimensionSize<2>());
    CPPUNIT_ASSERT_EQUAL(18_st, array3d.totalSize());
    array3d.at(0, 0, 0) = 'a';
    array3d.at(0, 0, 1) = 'b';
    array3d.at(0, 0, 2) = 'c';
    array3d.at(0, 1, 0) = 'd';
    array3d.at(0, 1, 1) = 'e';
    array3d.at(0, 1, 2) = 'f';
    array3d.at(1, 0, 0) = 'g';
    array3d.at(1, 0, 1) = 'h';
    array3d.at(1, 0, 2) = 'i';
    array3d.at(1, 1, 0) = 'j';
    array3d.at(1, 1, 1) = 'k';
    array3d.at(1, 1, 2) = 'l';
    array3d.at(2, 0, 0) = 'm';
    array3d.at(2, 0, 1) = 'n';
    array3d.at(2, 0, 2) = 'o';
    array3d.at(2, 1, 0) = 'p';
    array3d.at(2, 1, 1) = 'q';
    array3d.at(2, 1, 2) = 'r';
    CPPUNIT_ASSERT_EQUAL("abcdefghijklmnopqr"s, string(array3d.data(), 18));

    auto stackMultiArray(makeFixedSizeMultiArray<char, 9>(3, 3));
    CPPUNIT_ASSERT_EQUAL(3_st, stackMultiArray.dimensionSize<0>());
    CPPUNIT_ASSERT_EQUAL(3_st, stackMultiArray.dimensionSize<1>());
    CPPUNIT_ASSERT_EQUAL(9_st, stackMultiArray.totalSize());
    stackMultiArray.at(0, 0) = 'a';
    stackMultiArray.at(0, 1) = 'b';
    stackMultiArray.at(0, 2) = 'c';
    stackMultiArray.at(1, 0) = 'd';
    stackMultiArray.at(1, 1) = 'e';
    stackMultiArray.at(1, 2) = 'f';
    stackMultiArray.at(2, 0) = 'g';
    stackMultiArray.at(2, 1) = 'h';
    stackMultiArray.at(2, 2) = 'i';
    CPPUNIT_ASSERT_EQUAL("abcdefghi"s, string(stackMultiArray.data(), 9));
}

void MiscTests::testLevenshtein()
{
    CPPUNIT_ASSERT_EQUAL(1_st, computeDamerauLevenshteinDistance("ab", "abc"));
    CPPUNIT_ASSERT_EQUAL(1_st, computeDamerauLevenshteinDistance("abc", "ab"));
    CPPUNIT_ASSERT_EQUAL(2_st, computeDamerauLevenshteinDistance("xzaby", "xbay"));
    CPPUNIT_ASSERT_EQUAL(0_st, computeDamerauLevenshteinDistance("", ""));
    CPPUNIT_ASSERT_EQUAL(1_st, computeDamerauLevenshteinDistance("ab", "ba"));
    CPPUNIT_ASSERT_EQUAL(1_st, computeDamerauLevenshteinDistance("xaby", "xbay"));
    CPPUNIT_ASSERT_EQUAL(0_st, computeDamerauLevenshteinDistance("abc", "abc"));
    CPPUNIT_ASSERT_EQUAL(1_st, computeDamerauLevenshteinDistance("ab", "abc"));
    CPPUNIT_ASSERT_EQUAL(2_st, computeDamerauLevenshteinDistance("ca", "abc"));
    CPPUNIT_ASSERT_EQUAL(4_st, computeDamerauLevenshteinDistance("", "abcd"));
    CPPUNIT_ASSERT_EQUAL(4_st, computeDamerauLevenshteinDistance("abcd", ""));
    CPPUNIT_ASSERT_EQUAL(3_st, computeDamerauLevenshteinDistance("abcd", "d"));
    CPPUNIT_ASSERT_EQUAL(2_st, computeDamerauLevenshteinDistance("abcd", "bc"));
    CPPUNIT_ASSERT_EQUAL(3_st, computeDamerauLevenshteinDistance("abcd", "a"));
    CPPUNIT_ASSERT_EQUAL(2_st, computeDamerauLevenshteinDistance("adb", "abc"));
    CPPUNIT_ASSERT_EQUAL(2_st, computeDamerauLevenshteinDistance("xxaxx", "xxäxx"));
    CPPUNIT_ASSERT_EQUAL(1_st, computeDamerauLevenshteinDistance("xxöxx", "xxäxx"));
    CPPUNIT_ASSERT_EQUAL(11_st, computeDamerauLevenshteinDistance("this is a long text", "this is too long for stack"));
}

/*!
 * \brief Tests helper from TestUtilities namespace which aren't used in other tests anyways.
 */
void MiscTests::testTestUtilities()
{
    const auto workingCopyPathForNestedTestFile = workingCopyPath("subdir/nested-testfile.txt");
    CPPUNIT_ASSERT_EQUAL_MESSAGE("creation of subdirectories in working dir", "some file\n"s, readFile(workingCopyPathForNestedTestFile));

    const auto workingCopyPathUnderDifferentNameForNestedTestFile = workingCopyPathAs("subdir/nested-testfile.txt", "subdir2/foo.txt");
    const auto splittedPath = splitString<vector<string>>(workingCopyPathUnderDifferentNameForNestedTestFile, "/", EmptyPartsTreat::Omit);
    CPPUNIT_ASSERT_GREATEREQUAL(2_st, splittedPath.size());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("different subdir", "subdir2"s, splittedPath[splittedPath.size() - 2]);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("different file name", "foo.txt"s, splittedPath[splittedPath.size() - 1]);
    CPPUNIT_ASSERT_EQUAL_MESSAGE(
        "creation of subdirectories in working dir", "some file\n"s, readFile(workingCopyPathUnderDifferentNameForNestedTestFile));

    stringstream ss;
    ss << asHexNumber(16);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("printing hex numbers", "0x10"s, ss.str());

    TESTUTILS_ASSERT_LIKE("assert like works", ".*foo.*", "   foo   ");
}

#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
void MiscTests::testVerification()
{
    const auto key = std::string_view(
        R"(-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAWJAn1E7ZE5Q6H69oaV5sqCIppJdg
4bXDan9dJv6GOg70/t7q2CvwcwUXhV4FvCZxCHo25+rWYINfqKU2Utul8koAx8tK
59ohfOzI63I+CC76GfX41uRGU0P5i6hS7o/hgBLiVXqT0FgS2BMfmnLMUvUjqnI2
YQM7C55/5BM5Vrblkow=
-----END PUBLIC KEY-----)");
    const auto signature = std::string_view(
        R"(-----BEGIN SIGNATURE-----
MIGIAkIB+LB01DduBFMVs7Ea2McD7/kXpP0XktDNR7WpVgkOn4+/ilR8b8lpO9dd
FGmxKj5UVr2GpcWX6I216PjaVL9tr5oCQgFMpvNjSgFQ/KFaE+0d+QCegr3V7Uz6
sWB0iGdPa+oXbRish7HoNCU/k0lD3ffXaf8ueC78Zme9NFO18Ol+NWXJDA==
-----END SIGNATURE-----)");

    auto message = std::string("test message");
    CPPUNIT_ASSERT_EQUAL_MESSAGE("valid message", std::string(), verifySignature(key, signature, message));

    message[5] = '?';
    CPPUNIT_ASSERT_EQUAL_MESSAGE("manipulate message", "incorrect signature"s, verifySignature(key, signature, message));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("manipulate message", "incorrect signature"s, verifySignature(key, signature, message));

    CPPUNIT_ASSERT_EQUAL_MESSAGE(
        "verifySignature can be called with array of signatures", "incorrect signature"s, verifySignature(SigningKeys::openssl, signature, message));
    CPPUNIT_ASSERT_EQUAL_MESSAGE(
        "verifySignature can be called with empty vector", "no keys provided"s, verifySignature(std::vector<std::string_view>(), signature, message));
}
#endif

// test flagenumclass.h

namespace FlagEnumTests {
enum class TestFlags { None, Foo = 1, Bar = 2, Baz = 4, Biz = 8 };
}

CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(FlagEnumTests, FlagEnumTests::TestFlags);

namespace FlagEnumTests {

constexpr bool testFlagEnumClass()
{
    // test const operations
    const auto testFlags = TestFlags::Foo | TestFlags::Baz;
    static_assert((testFlags & TestFlags::Foo) == TestFlags::Foo);
    static_assert(testFlags && TestFlags::Foo);
    static_assert(!(testFlags && TestFlags::Bar));
    static_assert((testFlags & TestFlags::Bar) == TestFlags::None);
    static_assert(testFlags && TestFlags::Baz);
    static_assert((testFlags & TestFlags::Baz) == TestFlags::Baz);
    static_assert(checkFlagEnum(testFlags, TestFlags::Foo | TestFlags::Baz));
    static_assert(checkFlagEnum(testFlags, TestFlags::Foo));
    static_assert(!checkFlagEnum(testFlags, TestFlags::Foo | TestFlags::Bar));

    // test modifying
    auto nonConstFlags = TestFlags::Foo | TestFlags::Biz;
    modFlagEnum(nonConstFlags, TestFlags::Foo | TestFlags::Bar, false);
    nonConstFlags += TestFlags::Bar;
    modFlagEnum(nonConstFlags, TestFlags::Baz | TestFlags::Biz, true);
    return nonConstFlags == (TestFlags::Bar | TestFlags::Baz | TestFlags::Biz);
}
static_assert(testFlagEnumClass());

} // namespace FlagEnumTests
