/**
 * UGENE - Integrated Bioinformatics Tools.
 * Copyright (C) 2008-2024 UniPro <ugene@unipro.ru>
 * http://ugene.net
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include "AttributeDbiUnitTests.h"

#include <U2Core/DNAAlphabet.h>
#include <U2Core/U2AttributeDbi.h>
#include <U2Core/U2DbiUtils.h>
#include <U2Core/U2ObjectDbi.h>
#include <U2Core/U2OpStatusUtils.h>
#include <U2Core/U2SafePoints.h>
#include <U2Core/U2SqlHelpers.h>

namespace U2 {

static const QString ATTRIBUTE_NAMES = "attribute_names";

static const QString OBJECT_ATTRIBUTES = "object_attributes";

static const QString OBJECT_ATTRIBUTES_NAME = "object_attributes_name";

static const QString OBJECT_ATTRIBUTES_CHILD_ID = "object_attributes_child_id";

const QString& AttributeTestData::ATT_DB_URL("attribute-dbi.ugenedb");

U2AttributeDbi* AttributeTestData::attributeDbi = nullptr;
QList<U2DataId>* AttributeTestData::objects = nullptr;
TestDbiProvider AttributeTestData::dbiProvider = TestDbiProvider();

static bool registerTests() {
    qRegisterMetaType<U2::AttributeDbiUnitTests_ByteArrayAttribute>("AttributeDbiUnitTests_ByteArrayAttribute");
    qRegisterMetaType<U2::AttributeDbiUnitTests_getAvailableAttributeNames>("AttributeDbiUnitTests_getAvailableAttributeNames");
    qRegisterMetaType<U2::AttributeDbiUnitTests_getObjectAttributes>("AttributeDbiUnitTests_getObjectAttributes");
    qRegisterMetaType<U2::AttributeDbiUnitTests_getObjectAttributesByName>("AttributeDbiUnitTests_getObjectAttributesByName");
    qRegisterMetaType<U2::AttributeDbiUnitTests_getObjectPairAttributes>("AttributeDbiUnitTests_getObjectPairAttributes");
    qRegisterMetaType<U2::AttributeDbiUnitTests_getObjectPairAttributesByName>("AttributeDbiUnitTests_getObjectPairAttributesByName");
    qRegisterMetaType<U2::AttributeDbiUnitTests_IntegerAttribute>("AttributeDbiUnitTests_IntegerAttribute");
    qRegisterMetaType<U2::AttributeDbiUnitTests_RealAttribute>("AttributeDbiUnitTests_RealAttribute");
    qRegisterMetaType<U2::AttributeDbiUnitTests_removeAttributes>("AttributeDbiUnitTests_removeAttributes");
    qRegisterMetaType<U2::AttributeDbiUnitTests_removeObjectAttributes>("AttributeDbiUnitTests_removeObjectAttributes");
    qRegisterMetaType<U2::AttributeDbiUnitTests_StringAttribute>("AttributeDbiUnitTests_StringAttribute");
    return true;
}

bool AttributeTestData::registerTest = registerTests();

void AttributeTestData::init() {
    bool ok = dbiProvider.init(ATT_DB_URL, false);
    SAFE_POINT(ok, "dbi provider failed to initialize", );
    U2Dbi* dbi = dbiProvider.getDbi();
    U2ObjectDbi* objDbi = dbi->getObjectDbi();
    U2OpStatusImpl opStatus;

    objects = new QList<U2DataId>(objDbi->getObjects("/", 0, U2DbiOptions::U2_DBI_NO_LIMIT, opStatus));
    SAFE_POINT_OP(opStatus, );

    attributeDbi = dbi->getAttributeDbi();
    SAFE_POINT(attributeDbi != nullptr, "attribute database not loaded", );
}

U2AttributeDbi* AttributeTestData::getAttributeDbi() {
    if (attributeDbi == nullptr) {
        AttributeTestData::init();
    }
    return attributeDbi;
}

void AttributeTestData::shutdown() {
    if (attributeDbi != nullptr) {
        U2OpStatusImpl opStatus;
        dbiProvider.close();
        attributeDbi = nullptr;
        SAFE_POINT_OP(opStatus, );
    }
}

static bool compareAttributesBase(const U2Attribute& attr1, const U2Attribute& attr2) {
    if (attr1.objectId != attr2.objectId) {
        return false;
    }
    if (attr1.childId != attr2.childId) {
        return false;
    }
    if (attr1.version != attr2.version) {
        return false;
    }
    if (attr1.name != attr2.name) {
        return false;
    }
    return true;
}

template<class A>
static bool compareAttributes(const A& attr1, const A& attr2) {
    if (attr1.value != attr2.value) {
        return false;
    }
    return compareAttributesBase(attr1, attr2);
}

template<class T, typename Compare>
bool removeOne(QList<T>& list, const T& el, Compare compare) {
    QMutableListIterator<T> iter(list);
    while (iter.hasNext()) {
        const T& current = iter.next();
        if (compare(current, el)) {
            iter.remove();
            return true;
        }
    }
    return false;
}

template<class T>
void filterAttributesByName(QList<T>& attrs, const QString& name) {
    if (!name.isEmpty()) {
        QMutableListIterator<T> iter(attrs);
        while (iter.hasNext()) {
            const T& current = iter.next();
            if (current.name != name) {
                iter.remove();
            }
        }
    }
}

template<class T>
void filterAttributesByChildId(QList<T>& attrs, const U2DataId& childId) {
    QMutableListIterator<T> iter(attrs);
    while (iter.hasNext()) {
        const T& current = iter.next();
        if (current.childId != childId) {
            iter.remove();
        }
    }
}

void AttributeTestData::testAttributesMatch(QList<U2IntegerAttribute>& expectedInt,
                                            QList<U2RealAttribute>& expectedReal,
                                            QList<U2StringAttribute>& expectedString,
                                            QList<U2ByteArrayAttribute>& expectedArray,
                                            const QList<U2DataId>& attributes) {
    foreach (const U2DataId& attrId, attributes) {
        U2DataType type = U2DbiUtils::toType(attrId);
        SAFE_POINT(U2Type::isAttributeType(type), "inncorrect attribute type", );

        U2OpStatusImpl os;

        if (type == U2Type::AttributeInteger) {
            U2IntegerAttribute attr = getAttributeDbi()->getIntegerAttribute(attrId, os);
            SAFE_POINT_OP(os, );
            SAFE_POINT(removeOne(expectedInt, attr, compareAttributes<U2IntegerAttribute>), "Integer attribute is not removed", );
        } else if (type == U2Type::AttributeReal) {
            U2RealAttribute attr = getAttributeDbi()->getRealAttribute(attrId, os);
            SAFE_POINT_OP(os, );
            SAFE_POINT(removeOne(expectedReal, attr, compareAttributes<U2RealAttribute>), "Real attribute is not removed", );
        } else if (type == U2Type::AttributeString) {
            U2StringAttribute attr = getAttributeDbi()->getStringAttribute(attrId, os);
            SAFE_POINT_OP(os, );
            SAFE_POINT(removeOne(expectedString, attr, compareAttributes<U2StringAttribute>), "String attribute is not removed", );
        } else {  // AttributeByteArray
            U2ByteArrayAttribute attr = getAttributeDbi()->getByteArrayAttribute(attrId, os);
            SAFE_POINT_OP(os, );
            SAFE_POINT(removeOne(expectedArray, attr, compareAttributes<U2ByteArrayAttribute>), "ByteArray attribute is not removed", );
        }
    }

    SAFE_POINT(expectedInt.size() == 0, "expected integer list size should be 0", );
    SAFE_POINT(expectedReal.size() == 0, "expected real list size should be 0", );
    SAFE_POINT(expectedString.size() == 0, "expected string list size should be 0", );
    SAFE_POINT(expectedArray.size() == 0, "expected byte array list size should be 0", );
}

void AttributeTestData::getObjectAttrsTest(const APITestData& testData, bool filterByName, bool filterByChild) {
    const ObjectAttributesTestData& attrsData = testData.getValue<ObjectAttributesTestData>(OBJECT_ATTRIBUTES);

    const U2DataId& objectId = attrsData.objId;

    QList<U2IntegerAttribute> attrsInt = attrsData.intAttrs;
    QList<U2RealAttribute> attrsReal = attrsData.realAttrs;
    QList<U2StringAttribute> attrsString = attrsData.stringAttrs;
    QList<U2ByteArrayAttribute> attrsByteArray = attrsData.byteArrAttrs;

    const U2DataId& childId = testData.getValue<U2DataId>(OBJECT_ATTRIBUTES_CHILD_ID);
    QString name;

    if (filterByName) {
        name = testData.getValue<QString>(OBJECT_ATTRIBUTES_NAME);
        filterAttributesByName(attrsInt, name);
        filterAttributesByName(attrsReal, name);
        filterAttributesByName(attrsString, name);
        filterAttributesByName(attrsByteArray, name);
    }

    QList<U2DataId> attributes;
    U2OpStatusImpl os;
    if (filterByChild) {
        filterAttributesByChildId(attrsInt, childId);
        filterAttributesByChildId(attrsReal, childId);
        filterAttributesByChildId(attrsString, childId);
        filterAttributesByChildId(attrsByteArray, childId);
        attributes = getAttributeDbi()->getObjectPairAttributes(objectId, childId, name, os);
    } else {
        attributes = getAttributeDbi()->getObjectAttributes(objectId, name, os);
    }
    SAFE_POINT_OP(os, );

    testAttributesMatch(attrsInt, attrsReal, attrsString, attrsByteArray, attributes);
}

void AttributeDbiUnitTests_getAvailableAttributeNames::Test() {
    U2AttributeDbi* attributeDbi = AttributeTestData::getAttributeDbi();

    QStringList names;
    names << "int1"
          << "int2"
          << "real1"
          << "str1"
          << "str2"
          << "arr1";

    U2OpStatusImpl os;
    QStringList actual = attributeDbi->getAvailableAttributeNames(os);
    CHECK_NO_ERROR(os);

    CHECK_TRUE(actual.size() == names.size(), "incorrect expected available attribute names size");
    foreach (const QString& name, names) {
        CHECK_TRUE(actual.removeOne(name), "available attribute name not removed");
    }
    CHECK_TRUE(actual.size() == 0, "available attribute names size should be 0");
}

void AttributeDbiUnitTests_getObjectAttributes::Test() {
    const U2DataId& childId = U2DbiUtils::toU2DataId(2, U2Type::Assembly);
    APITestData testData;

    // getObjectAttributes
    ObjectAttributesTestData oat;
    {
        oat.objId = U2DbiUtils::toU2DataId(1, U2Type::Sequence);

        {
            U2IntegerAttribute attr;
            attr.objectId = oat.objId;
            attr.name = "int1";
            attr.value = 5;
            attr.childId = childId;
            oat.intAttrs.append(attr);
        }

        {
            U2IntegerAttribute attr;
            attr.objectId = oat.objId;
            attr.name = "int2";
            attr.value = 3;
            attr.childId = childId;
            oat.intAttrs.append(attr);
        }

        {
            U2RealAttribute attr;
            attr.objectId = oat.objId;
            attr.name = "real1";
            attr.value = 2.7;
            attr.childId = childId;
            oat.realAttrs.append(attr);
        }

        {
            U2StringAttribute attr;
            attr.objectId = oat.objId;
            attr.name = "str1";
            attr.value = "some string";
            oat.stringAttrs.append(attr);
        }

        {
            U2StringAttribute attr;
            attr.objectId = oat.objId;
            attr.name = "str2";
            attr.value = "other string";
            oat.stringAttrs.append(attr);
        }

        {
            U2ByteArrayAttribute attr;
            attr.objectId = oat.objId;
            attr.name = "arr1";
            attr.value = "some array";
            oat.byteArrAttrs.append(attr);
        }
    }

    testData.addValue(OBJECT_ATTRIBUTES, oat);
    AttributeTestData::getObjectAttrsTest(testData, false, false);
}

void AttributeDbiUnitTests_getObjectAttributesByName::Test() {
    APITestData testData;
    testData.addValue<QString>(OBJECT_ATTRIBUTES_NAME, "int2");
    AttributeTestData::getObjectAttrsTest(testData, true, false);
}

void AttributeDbiUnitTests_getObjectPairAttributes::Test() {
    APITestData testData;
    const U2DataId& childId = U2DbiUtils::toU2DataId(2, U2Type::Assembly);
    testData.addValue<U2DataId>(OBJECT_ATTRIBUTES_CHILD_ID, childId);
    AttributeTestData::getObjectAttrsTest(testData, false, true);
}

void AttributeDbiUnitTests_getObjectPairAttributesByName::Test() {
    APITestData testData;
    testData.addValue<QString>(OBJECT_ATTRIBUTES_NAME, "int2");
    const U2DataId& childId = U2DbiUtils::toU2DataId(2, U2Type::Assembly);
    testData.addValue<U2DataId>(OBJECT_ATTRIBUTES_CHILD_ID, childId);
    AttributeTestData::getObjectAttrsTest(testData, true, true);
}

void AttributeDbiUnitTests_removeAttributes::Test() {
    U2AttributeDbi* attributeDbi = AttributeTestData::getAttributeDbi();

    const U2DataId& objectId = AttributeTestData::getObjects()->first();
    U2OpStatusImpl os;
    const QList<U2DataId>& attrs1 = attributeDbi->getObjectAttributes(objectId, "", os);
    CHECK_NO_ERROR(os);

    attributeDbi->removeAttributes(attrs1, os);
    CHECK_NO_ERROR(os);

    const QList<U2DataId>& attrs2 = attributeDbi->getObjectAttributes(objectId, "", os);
    CHECK_NO_ERROR(os);
    CHECK_TRUE(attrs2.isEmpty(), "attribute list should be empty");
}

void AttributeDbiUnitTests_removeObjectAttributes::Test() {
    U2AttributeDbi* attributeDbi = AttributeTestData::getAttributeDbi();

    const U2DataId& objectId = AttributeTestData::getObjects()->first();
    {
        U2OpStatusImpl os;
        attributeDbi->removeObjectAttributes(objectId, os);
        CHECK_NO_ERROR(os);
    }
    U2OpStatusImpl os;
    const QList<U2DataId>& attrs = attributeDbi->getObjectAttributes(objectId, "", os);
    CHECK_NO_ERROR(os);
    CHECK_TRUE(attrs.isEmpty(), "object attribute list should be empty");
}

void AttributeDbiUnitTests_IntegerAttribute::Test() {
    U2AttributeDbi* attributeDbi = AttributeTestData::getAttributeDbi();

    U2IntegerAttribute attr;
    attr.objectId = AttributeTestData::getObjects()->first();
    attr.value = 46;

    {
        U2OpStatusImpl os;
        attributeDbi->createIntegerAttribute(attr, os);
        CHECK_NO_ERROR(os);
    }

    U2OpStatusImpl os;
    U2IntegerAttribute actual = attributeDbi->getIntegerAttribute(attr.id, os);
    CHECK_NO_ERROR(os);
    CHECK_TRUE(compareAttributes(actual, attr), "incorrect integer attribute");
}

void AttributeDbiUnitTests_RealAttribute::Test() {
    U2AttributeDbi* attributeDbi = AttributeTestData::getAttributeDbi();

    U2RealAttribute attr;
    attr.objectId = AttributeTestData::getObjects()->first();
    attr.value = 46.46;

    {
        U2OpStatusImpl os;
        attributeDbi->createRealAttribute(attr, os);
        CHECK_NO_ERROR(os);
    }

    U2OpStatusImpl os;
    U2RealAttribute actual = attributeDbi->getRealAttribute(attr.id, os);
    CHECK_NO_ERROR(os);
    CHECK_TRUE(compareAttributes(actual, attr), "incorrect real attribute");
}

void AttributeDbiUnitTests_StringAttribute::Test() {
    U2AttributeDbi* attributeDbi = AttributeTestData::getAttributeDbi();

    U2StringAttribute attr;
    attr.objectId = AttributeTestData::getObjects()->first();
    attr.value = "some string";

    {
        U2OpStatusImpl os;
        attributeDbi->createStringAttribute(attr, os);
        CHECK_NO_ERROR(os);
    }

    U2OpStatusImpl os;
    U2StringAttribute actual = attributeDbi->getStringAttribute(attr.id, os);
    CHECK_NO_ERROR(os);
    CHECK_TRUE(compareAttributes(actual, attr), "incorrect string attribute");
}

void AttributeDbiUnitTests_ByteArrayAttribute::Test() {
    U2AttributeDbi* attributeDbi = AttributeTestData::getAttributeDbi();

    U2ByteArrayAttribute attr;
    attr.objectId = AttributeTestData::getObjects()->first();
    attr.value = "some array";

    {
        U2OpStatusImpl os;
        attributeDbi->createByteArrayAttribute(attr, os);
        CHECK_NO_ERROR(os);
    }

    U2OpStatusImpl os;
    U2ByteArrayAttribute actual = attributeDbi->getByteArrayAttribute(attr.id, os);
    CHECK_NO_ERROR(os);
    CHECK_TRUE(compareAttributes(actual, attr), "incorrect byte array attribute");
}

}  // namespace U2
