// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtTest>

#include <QtCore/qjnitypes.h>
#include <QtCore/qjniarray.h>

using namespace Qt::StringLiterals;

class tst_QJniArray : public QObject
{
    Q_OBJECT

public:
    tst_QJniArray() = default;

private slots:
    void construct();
    void copyAndMove();
    void invalidArraysAreEmpty();
    void size();
    void operators();
    void ordering();
    void toContainer();
    void pointerToValue();
};

using namespace QtJniTypes;

// fake type so that we can compile-time test and assert correct type mappings
Q_DECLARE_JNI_CLASS(List, "qt/test/List");

// verify that we get the return type we expect for the specified return type
#define VERIFY_RETURN_FOR_TYPE(In, Out) \
static_assert(std::is_same_v<decltype(List::callStaticMethod<In>("toByteArray")), Out>)

VERIFY_RETURN_FOR_TYPE(jobjectArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jobject[], QJniArray<jobject>);
//VERIFY_RETURN_FOR_TYPE(QList<QJniObject>, QList<QJniObject>);
VERIFY_RETURN_FOR_TYPE(QList<jobject>, QList<jobject>);

VERIFY_RETURN_FOR_TYPE(jbyteArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jbyte[], QJniArray<jbyte>);
VERIFY_RETURN_FOR_TYPE(QByteArray, QByteArray);

VERIFY_RETURN_FOR_TYPE(jbooleanArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jboolean[], QJniArray<jboolean>);
VERIFY_RETURN_FOR_TYPE(QList<jboolean>, QList<jboolean>);
VERIFY_RETURN_FOR_TYPE(QList<bool>, QList<jboolean>);

VERIFY_RETURN_FOR_TYPE(jcharArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jchar[], QJniArray<jchar>);
VERIFY_RETURN_FOR_TYPE(QList<jchar>, QList<jchar>);

VERIFY_RETURN_FOR_TYPE(jshortArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jshort[], QJniArray<jshort>);
VERIFY_RETURN_FOR_TYPE(QList<jshort>, QList<jshort>);
VERIFY_RETURN_FOR_TYPE(QList<short>, QList<short>);

VERIFY_RETURN_FOR_TYPE(jintArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jint[], QJniArray<jint>);
VERIFY_RETURN_FOR_TYPE(QList<jint>, QList<jint>);
VERIFY_RETURN_FOR_TYPE(QList<int>, QList<int>);

VERIFY_RETURN_FOR_TYPE(jlongArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jlong[], QJniArray<jlong>);
VERIFY_RETURN_FOR_TYPE(QList<jlong>, QList<jlong>);
// VERIFY_RETURN_FOR_TYPE(QList<long>, QList<long>); // assumes long is 64bit

VERIFY_RETURN_FOR_TYPE(jfloatArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jfloat[], QJniArray<jfloat>);
VERIFY_RETURN_FOR_TYPE(QList<jfloat>, QList<jfloat>);
VERIFY_RETURN_FOR_TYPE(QList<float>, QList<float>);

VERIFY_RETURN_FOR_TYPE(jdoubleArray, QJniObject);
VERIFY_RETURN_FOR_TYPE(jdouble[], QJniArray<jdouble>);
VERIFY_RETURN_FOR_TYPE(QList<jdouble>, QList<jdouble>);
VERIFY_RETURN_FOR_TYPE(QList<double>, QList<double>);

VERIFY_RETURN_FOR_TYPE(QString, QString);
VERIFY_RETURN_FOR_TYPE(QJniArray<QString>, QJniArray<QString>);

VERIFY_RETURN_FOR_TYPE(List, List);
VERIFY_RETURN_FOR_TYPE(List[], QJniArray<List>);
VERIFY_RETURN_FOR_TYPE(QJniArray<List>, QJniArray<List>);

#define VERIFY_CONTAINER_FOR_TYPE(In, Out) \
    static_assert(std::is_same_v<decltype(std::declval<In>().toContainer()), Out>);

#define VERIFY_ARRAY_FOR_CONTAINER(In, Out) \
    static_assert(std::is_same_v<decltype(QJniArrayBase::fromContainer(std::declval<In>())), Out>);

// primitive types, both JNI and C++ equivalent types
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jchar>, QList<jchar>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jchar>, QJniArray<jchar>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jint>, QList<jint>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jint>, QJniArray<jint>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<int>, QList<int>);
VERIFY_ARRAY_FOR_CONTAINER(QList<int>, QJniArray<int>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jfloat>, QList<jfloat>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jfloat>, QJniArray<jfloat>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<float>, QList<float>);
VERIFY_ARRAY_FOR_CONTAINER(QList<float>, QJniArray<float>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jdouble>, QList<jdouble>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jdouble>, QJniArray<jdouble>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<double>, QList<double>);
VERIFY_ARRAY_FOR_CONTAINER(QList<double>, QJniArray<double>);

// jobject and derivatives
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jobject>, QList<jobject>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jobject>, QJniArray<jobject>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jclass>, QList<jclass>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jclass>, QJniArray<jclass>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jthrowable>, QList<jthrowable>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jthrowable>, QJniArray<jthrowable>);

// QJniObject-ish classes
VERIFY_CONTAINER_FOR_TYPE(QJniArray<QJniObject>, QList<QJniObject>);
VERIFY_ARRAY_FOR_CONTAINER(QList<QJniObject>, QJniArray<QJniObject>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<List>, QList<List>);
VERIFY_ARRAY_FOR_CONTAINER(QList<List>, QJniArray<List>);

// Special case: jbyte, (u)char, and QByteArray
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jbyte>, QByteArray);
VERIFY_ARRAY_FOR_CONTAINER(QByteArray, QJniArray<jbyte>);
VERIFY_ARRAY_FOR_CONTAINER(QList<jbyte>, QJniArray<jbyte>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<char>, QByteArray);
VERIFY_ARRAY_FOR_CONTAINER(QList<char>, QJniArray<jbyte>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<uchar>, QList<uchar>);

// Special case: jstring, QString, and QStringList
VERIFY_CONTAINER_FOR_TYPE(QJniArray<jstring>, QStringList);
VERIFY_ARRAY_FOR_CONTAINER(QStringList, QJniArray<QString>);
VERIFY_CONTAINER_FOR_TYPE(QJniArray<QString>, QStringList);
VERIFY_ARRAY_FOR_CONTAINER(QList<jstring>, QJniArray<jstring>);

void tst_QJniArray::construct()
{
    // explicit
    {
        QStringList strings;
        for (int i = 0; i < 10000; ++i)
            strings << QString::number(i);
        QJniArray<QString> list(strings);
        QCOMPARE(list.size(), 10000);
        QCOMPARE(list.at(500), QString::number(500));
        QCOMPARE(list.toContainer(), strings);
    }
    {
        QJniArray bytes = QJniArrayBase::fromContainer(QByteArray("abc"));
        static_assert(std::is_same_v<decltype(bytes)::value_type, jbyte>);
        QCOMPARE(bytes.size(), 3);
    }
    {
        QJniArray list{1, 2, 3};
        static_assert(std::is_same_v<decltype(list), QJniArray<int>>);
        QCOMPARE(list.size(), 3);
        list = {4, 5};
        QCOMPARE(list.size(), 2);
    }
    {
        QJniArray<jint> list(QList<int>{1, 2, 3});
        QCOMPARE(list.size(), 3);
    }
    // CTAD with deduction guide
    {
        QJniArray list(QList<int>{1, 2, 3});
        QCOMPARE(list.size(), 3);
    }
    {
        QJniArray bytes(QByteArray("abc"));
        static_assert(std::is_same_v<decltype(bytes)::value_type, jbyte>);
        QCOMPARE(bytes.size(), 3);
    }
    {
        QStringList strings{"a", "b", "c"};
        QJniArray list(strings);
        QCOMPARE(list.size(), 3);
    }
    {
        QJniArray<jint> list{QList<int>{1, 2, 3}};
        QCOMPARE(list.size(), 3);
    }

    // non-contiguous container
    {
       std::list list({1, 2, 3});
       QJniArray array(list);
       QCOMPARE(array.size(), 3);
    }
    {
        std::list list({QString(), QString(), QString()});
        QJniArray array(list);
        QCOMPARE(array.size(), 3);
    }
}

// Verify that we can convert QJniArrays into each other as long as element types
// are convertible without narrowing.
template <typename From, typename To>
using CanConstructDetector = decltype(QJniArray<To>(std::declval<QJniArray<From>>()));
template <typename From, typename To>
using CanAssignDetector = decltype(std::declval<QJniArray<To>>().operator=(std::declval<QJniArray<From>>()));

template <typename From, typename To>
static constexpr bool canConstruct = qxp::is_detected_v<CanConstructDetector, From, To>;
template <typename From, typename To>
static constexpr bool canAssign = qxp::is_detected_v<CanAssignDetector, From, To>;

static_assert(canConstruct<jshort, jint> && canAssign<jshort, jint>);
static_assert(!canConstruct<jint, jshort> && !canAssign<jint, jshort>);
static_assert(canConstruct<jstring, jobject> && canAssign<jstring, jobject>);
static_assert(!canConstruct<jobject, jstring> && !canAssign<jobject, jstring>);

// exercise the QJniArray(QJniArray<Other> &&other) constructor
void tst_QJniArray::copyAndMove()
{
    QJniArray<jshort> tempShortArray({1, 2, 3});

    // copy - both arrays remain valid and reference the same object
    {
        QJniArray<jshort> shortArrayCopy(tempShortArray);
        QVERIFY(tempShortArray.isValid());
        QVERIFY(shortArrayCopy.isValid());
        QCOMPARE(tempShortArray, shortArrayCopy);
    }

    // moving QJniArray<T> to QJniArray<T> leaves the moved-from object invalid
    QJniArray<jshort> shortArray(std::move(tempShortArray));
    QVERIFY(!tempShortArray.isValid());
    QVERIFY(shortArray.isValid());

    tempShortArray = shortArray;

    // copying QJniArray<short> to QJniArray<int> works
    QJniArray<jint> intArray(shortArray);
    QVERIFY(shortArray.isValid());
    QVERIFY(intArray.isValid());
    QCOMPARE(intArray, shortArray);

    // moving QJniArray<short> to QJniArray<int> leaves the moved-from array invalid
    QJniArray<jlong> longArray(std::move(shortArray));
    QVERIFY(!shortArray.isValid());
    QVERIFY(longArray.isValid());
    QCOMPARE(longArray, intArray);
    QCOMPARE_NE(longArray, shortArray); // we can compare a moved-from object

    longArray = intArray;

    // not possible due to narrowing conversion, covered by static_asserts above
    // QJniArray<jshort> shortArray2(longArray);
    // intArray = longArray;
}

void tst_QJniArray::invalidArraysAreEmpty()
{
    QJniArray<jchar> invalid;
    QVERIFY(!invalid.isValid());
    QCOMPARE(invalid.object(), nullptr);
    QVERIFY(invalid.isEmpty());

    QCOMPARE(invalid.begin(), invalid.end());
    QCOMPARE(invalid.rbegin(), invalid.rend());

    QList<jchar> data;
    // safe to iterate
    for (const auto &e : invalid)
        data.emplace_back(e);
    QVERIFY(data.empty());

    // safe to convert
    data = invalid.toContainer();
    QVERIFY(data.empty());

    // unsafe to access
    // auto element = invalid.at(0);
}

void tst_QJniArray::size()
{
    QJniArray<jint> array;
    QVERIFY(!array.isValid());
    QCOMPARE(array.size(), 0);

    QList<int> intList;
    intList.resize(10);
    auto intArray = QJniArray(intList);
    QCOMPARE(intArray.size(), 10);
}

void tst_QJniArray::operators()
{
    QByteArray bytes("abcde");
    QJniArray<jbyte> array(bytes);
    QVERIFY(array.isValid());

    {
        auto it = array.begin();
        QCOMPARE(*it, 'a');
        QCOMPARE(*++it, 'b');
        QCOMPARE(*it++, 'b');
        QCOMPARE(*it, 'c');
        ++it;
        it++;
        QCOMPARE(*it, 'e');
        QCOMPARE(++it, array.end());

        it -= array.size();
        QCOMPARE(it, array.begin());
        it += 2;
        QCOMPARE(*it, 'c');

        const auto it2 = it + 2;
        QCOMPARE(*it2, 'e');
        QCOMPARE(it2 - it, 2);

        it = it2 - 2;
        QCOMPARE(*it, 'c');

        it = 1 + it;
        QCOMPARE(*it, 'd');
        it = array.size() - it;
        QCOMPARE(*it, 'c');

        QCOMPARE(it[1], 'd');
        QCOMPARE(it[-1], 'b');
    }
    {
        auto it = array.rbegin();
        QCOMPARE(*it, 'e');
        QCOMPARE(*++it, 'd');
        QCOMPARE(*it++, 'd');
        QCOMPARE(*it, 'c');
        ++it;
        it++;
        QCOMPARE(*it, 'a');
        QCOMPARE(++it, array.rend());
    }
    {
        QJniArray<jbyte>::const_iterator it = {};
        QCOMPARE(it, QJniArray<jbyte>::const_iterator{});
        QCOMPARE_NE(array.begin(), array.end());

        it = array.begin();
        QCOMPARE(it, array.begin());
    }

    QCOMPARE(std::distance(array.begin(), array.end()), array.size());

    qsizetype index = 0;
    for (const auto &value : array) {
        QCOMPARE(value, bytes.at(index));
        ++index;
    }
}

void tst_QJniArray::ordering()
{
    QByteArray bytes("abcde");
    QJniArray array(bytes);

    auto arrayBegin = array.begin();
    auto arrayEnd = array.end();
    QCOMPARE(arrayBegin, arrayBegin);
    QCOMPARE_LT(arrayBegin, arrayEnd);
    QCOMPARE_GT(arrayEnd, arrayBegin);
}

template <typename T, typename C>
using ToContainerTest = decltype(std::declval<QJniArray<T>>().toContainer(std::declval<C>()));

template <typename T, typename C>
static constexpr bool hasToContainer = qxp::is_detected_v<ToContainerTest, T, C>;

static_assert(hasToContainer<jint, QList<jint>>);
static_assert(hasToContainer<jint, QList<int>>);

static_assert(hasToContainer<jbyte, QByteArray>);
static_assert(hasToContainer<jstring, QStringList>);
static_assert(hasToContainer<String, QStringList>);
static_assert(hasToContainer<jchar, std::list<jchar>>);
static_assert(hasToContainer<jbyte, std::vector<jbyte>>);
// different types but doesn't narrow
static_assert(hasToContainer<jshort, QList<int>>);
static_assert(hasToContainer<jfloat, QList<jdouble>>);
// would narrow
static_assert(!hasToContainer<jlong, QList<short>>);
static_assert(!hasToContainer<jdouble, QList<jfloat>>);
// incompatible types
static_assert(!hasToContainer<jstring, QByteArray>);

void tst_QJniArray::toContainer()
{
    std::vector<jchar> charVector{u'a', u'b', u'c'};
    QJniArray<jchar> charArray(charVector);

    std::vector<jchar> vector;
    charArray.toContainer(vector);

    QCOMPARE(vector, charVector);
    QCOMPARE(charArray.toContainer<std::vector<jchar>>(), charVector);

    // non-contiguous container of primitive elements
    std::list charList = charArray.toContainer<std::list<jchar>>();
    QCOMPARE(charList.size(), charVector.size());
}

void tst_QJniArray::pointerToValue()
{
    const QJniArray stringArray{u"one"_s, u"two"_s, u"three"_s};
    auto it = std::find(stringArray.begin(), stringArray.end(), u"two"_s);
    QCOMPARE_NE(it, stringArray.end());
    QCOMPARE(it->size(), 3);
    QCOMPARE((++it)->size(), 5);
}

QTEST_MAIN(tst_QJniArray)

#include "tst_qjniarray.moc"
