/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

require('jasmine-expect');
const JasmineReporters = require('jasmine-reporters');

const Util = require('util');
const exec = require('child_process').exec;
const config = require('./config');
const IgniteClient = require('apache-ignite-client');
const IgniteClientConfiguration = IgniteClient.IgniteClientConfiguration;
const Errors = IgniteClient.Errors;
const EnumItem = IgniteClient.EnumItem;
const Timestamp = IgniteClient.Timestamp;
const Decimal = IgniteClient.Decimal;
const BinaryObject = IgniteClient.BinaryObject;
const ObjectType = IgniteClient.ObjectType;

const TIMEOUT_MS = 60000;

jasmine.getEnv().addReporter(new JasmineReporters.TeamCityReporter());

const dateComparator = (date1, date2) => { return !date1 && !date2 || date1.value === date2.value; };
const floatComparator = (date1, date2) => { return Math.abs(date1 - date2) < 0.00001; };
const defaultComparator = (value1, value2) => { return value1 === value2; };
const enumComparator = (value1, value2) => {
    return value1.getTypeId() === value2.getTypeId() &&
        value1.getOrdinal() === value2.getOrdinal(); };
const decimalComparator = (value1, value2) => {
    return value1 === null && value2 === null ||
        value1.equals(value2);
};
const timestampComparator = (value1, value2) => {
    return value1 === null && value2 === null ||
        dateComparator(value1.getTime(), value2.getTime()) &&
        value1.getNanos() === value2.getNanos(); };

const numericValueModificator = (data) => { return data > 0 ? data - 10 : data + 10; };
const charValueModificator = (data) => { return String.fromCharCode(data.charCodeAt(0) + 5); };
const booleanValueModificator = (data) => { return !data; };
const stringValueModificator = (data) => { return data + 'xxx'; };
const dateValueModificator = (data) => { return new Date(data.getTime() + 12345); };
const UUIDValueModificator = (data) => { return data.reverse(); };
const enumValueModificator = (data) => { return new EnumItem(data.getTypeId(), data.getOrdinal() + 1); };
const decimalValueModificator = (data) => { return data.add(12345); };
const timestampValueModificator = (data) => { return new Timestamp(new Date(data.getTime() + 12345), data.getNanos() + 123); };

const primitiveValues = {
    [ObjectType.PRIMITIVE_TYPE.BYTE] : {
        values : [-128, 0, 127],
        isMapKey : true,
        modificator : numericValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.SHORT] : {
        values : [-32768, 0, 32767],
        isMapKey : true,
        modificator : numericValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.INTEGER] : {
        values : [12345, 0, -54321],
        isMapKey : true,
        modificator : numericValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.LONG] : {
        values : [12345678912345, 0, -98765432112345],
        isMapKey : true,
        modificator : numericValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.FLOAT] : {
        values : [-1.155, 0, 123e-5],
        isMapKey : false,
        modificator : numericValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.DOUBLE] : {
        values : [-123e5, 0, 0.0001],
        typeOptional : true,
        isMapKey : false,
        modificator : numericValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.CHAR] : {
        values : ['a', String.fromCharCode(0x1234)],
        isMapKey : true,
        modificator : charValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.BOOLEAN] : {
        values : [true, false],
        isMapKey : true,
        typeOptional : true,
        modificator : booleanValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.STRING] : {
        values : ['abc', ''],
        isMapKey : true,
        typeOptional : true,
        modificator : stringValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.UUID] : {
        values : [
            [ 18, 70, 2, 119, 154, 254, 198, 254, 195, 146, 33, 60, 116, 230, 0, 146 ],
            [ 141, 77, 31, 194, 127, 36, 184, 255, 192, 4, 118, 57, 253, 209, 111, 147 ]
        ],
        isMapKey : false,
        modificator : UUIDValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.DATE] : {
        values : [new Date(), new Date('1995-12-17T03:24:00'), new Date(0)],
        typeOptional : true,
        isMapKey : false,
        modificator : dateValueModificator
    },
    // [ObjectType.PRIMITIVE_TYPE.ENUM] : {
    //     values : [new EnumItem(12345, 7), new EnumItem(0, 0)],
    //     typeOptional : true,
    //     isMapKey : false,
    //     modificator : enumValueModificator
    // },
    [ObjectType.PRIMITIVE_TYPE.DECIMAL] : {
        values : [new Decimal('123456789.6789345'), new Decimal(0), new Decimal('-98765.4321e15')],
        typeOptional : true,
        isMapKey : false,
        modificator : decimalValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.TIMESTAMP] : {
        values : [new Timestamp(new Date().getTime(), 12345), new Timestamp(new Date('1995-12-17T03:24:00').getTime(), 543), new Timestamp(0, 0)],
        typeOptional : true,
        isMapKey : false,
        modificator : timestampValueModificator
    },
    [ObjectType.PRIMITIVE_TYPE.TIME] : {
        values : [new Date(), new Date('1995-12-17T03:24:00'), new Date(123)],
        isMapKey : false,
        modificator : dateValueModificator
    }
};

const arrayValues = {
    [ObjectType.PRIMITIVE_TYPE.BYTE_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.BYTE },
    [ObjectType.PRIMITIVE_TYPE.SHORT_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.SHORT },
    [ObjectType.PRIMITIVE_TYPE.INTEGER_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.INTEGER },
    [ObjectType.PRIMITIVE_TYPE.LONG_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.LONG },
    [ObjectType.PRIMITIVE_TYPE.FLOAT_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.FLOAT },
    [ObjectType.PRIMITIVE_TYPE.DOUBLE_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.DOUBLE, typeOptional : true },
    [ObjectType.PRIMITIVE_TYPE.CHAR_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.CHAR },
    [ObjectType.PRIMITIVE_TYPE.BOOLEAN_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.BOOLEAN, typeOptional : true },
    [ObjectType.PRIMITIVE_TYPE.STRING_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.STRING, typeOptional : true },
    [ObjectType.PRIMITIVE_TYPE.UUID_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.UUID },
    [ObjectType.PRIMITIVE_TYPE.DATE_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.DATE, typeOptional : true },
    //[ObjectType.PRIMITIVE_TYPE.ENUM_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.ENUM, typeOptional : true },
    [ObjectType.PRIMITIVE_TYPE.DECIMAL_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.DECIMAL, typeOptional : true },
    [ObjectType.PRIMITIVE_TYPE.TIMESTAMP_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.TIMESTAMP, typeOptional : true },
    [ObjectType.PRIMITIVE_TYPE.TIME_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.TIME }
};

// Helper class for testing apache-ignite-client library.
// Contains common methods for testing environment initialization and cleanup.
class TestingHelper {
    static get TIMEOUT() {
        return TIMEOUT_MS;
    }

    static get primitiveValues() {
        return primitiveValues;
    }

    static get arrayValues() {
        return arrayValues;
    }

    // Initializes testing environment: creates and starts the library client, sets default jasmine test timeout.
    // Should be called from any test suite beforeAll method.
    static async init() {
        jasmine.DEFAULT_TIMEOUT_INTERVAL = TIMEOUT_MS;

        TestingHelper._igniteClient = new IgniteClient();
        TestingHelper._igniteClient.setDebug(config.debug);
        await TestingHelper._igniteClient.connect(new IgniteClientConfiguration(...config.endpoints));
    }

    // Cleans up testing environment.
    // Should be called from any test suite afterAll method.
    static async cleanUp() {
        await TestingHelper.igniteClient.disconnect();
    }

    static get igniteClient() {
        return TestingHelper._igniteClient;
    }

    static async destroyCache(cacheName, done) {
        try {
            await TestingHelper.igniteClient.destroyCache(cacheName);
        }
        catch (err) {
            TestingHelper.checkOperationError(err, done);
        }
    }

    static executeExample(name, outputChecker) {
        return new Promise((resolve, reject) => {
                exec('node ' + name, (error, stdout, stderr) => {
                    TestingHelper.logDebug(stdout);
                    resolve(stdout);
                })
            }).
            then(output => {
                expect(output).not.toMatch('ERROR:');
                expect(output).toMatch('Client is started');
            });
    }

    static checkOperationError(error, done) {
        TestingHelper.checkError(error, Errors.OperationError, done)
    }

    static checkIllegalArgumentError(error, done) {
        TestingHelper.checkError(error, Errors.IgniteClientError, done)
    }

    static checkEnumItemSerializationError(error, done) {
        if (!(error instanceof Errors.IgniteClientError) ||
            error.message.indexOf('Enum item can not be serialized') < 0) {
            done.fail('unexpected error: ' + error);
        }
    }

    static checkError(error, errorType, done) {
        if (!(error instanceof errorType)) {
            done.fail('unexpected error: ' + error);
        }
    }

    static logDebug(message) {
        if (config.debug) {
            console.log(message);
        }
    }

    static printValue(value) {
        const val = Util.inspect(value, false, null);
        const length = 500;
        return val.length > length ? val.substr(0, length) + '...' : val;
    }

    static async compare(value1, value2) {
        TestingHelper.logDebug(Util.format('compare: %s and %s', TestingHelper.printValue(value1), TestingHelper.printValue(value2)));
        if (value1 === undefined || value2 === undefined) {
            TestingHelper.logDebug(Util.format('compare: unexpected "undefined" value'));
            return false;
        }
        if (value1 === null && value2 === null) {
            return true;
        }
        if (value1 === null && value2 !== null || value1 !== null && value2 === null) {
            return false;
        }
        if (typeof value1 !== typeof value2) {
            TestingHelper.logDebug(Util.format('compare: value types are different: %s and %s',
                typeof value1, typeof value2));
            return false;
        }
        if (typeof value1 === 'number') {
            return floatComparator(value1, value2);
        }
        else if (typeof value1 !== 'object') {
            return defaultComparator(value1, value2);
        }
        else if (value1.constructor.name !== value2.constructor.name && !value2 instanceof BinaryObject) {
            TestingHelper.logDebug(Util.format('compare: value types are different: %s and %s',
                value1.constructor.name, value2.constructor.name));
            return false;
        }
        else if (value1 instanceof Date && value2 instanceof Date) {
            return dateComparator(value1, value2);
        }
        else if (value1 instanceof EnumItem && value2 instanceof EnumItem) {
            return enumComparator(value1, value2);
        }
        else if (value1 instanceof Decimal && value2 instanceof Decimal) {
            return decimalComparator(value1, value2);
        }
        else if (value1 instanceof Timestamp && value2 instanceof Timestamp) {
            return timestampComparator(value1, value2);
        }
        else if (value1 instanceof Array && value2 instanceof Array) {
            if (value1.length !== value2.length) {
                TestingHelper.logDebug(Util.format('compare: array lengths are different'));
                return false;
            }
            for (var i = 0; i < value1.length; i++) {
                if (!await TestingHelper.compare(value1[i], value2[i])) {
                    TestingHelper.logDebug(Util.format('compare: array elements are different: %s, %s',
                        TestingHelper.printValue(value1[i]), TestingHelper.printValue(value2[i])));
                    return false;
                }
            }
            return true;
        }
        else if (value1 instanceof Map && value2 instanceof Map) {
            if (value1.size !== value2.size) {
                TestingHelper.logDebug(Util.format('compare: map sizes are different'));
                return false;
            }
            for (var [key, val] of value1) {
                if (!value2.has(key)) {
                    TestingHelper.logDebug(Util.format('compare: maps are different: %s key is absent', TestingHelper.printValue(key)));
                    return false;
                }
                if (!(await TestingHelper.compare(val, value2.get(key)))) {
                    TestingHelper.logDebug(Util.format('compare: map values are different: %s, %s',
                        TestingHelper.printValue(val), TestingHelper.printValue(value2.get(key))));
                    return false;
                }
            }
            return true;
        }
        else if (value1 instanceof Set && value2 instanceof Set) {
            if (value1.size !== value2.size) {
                TestingHelper.logDebug(Util.format('compare: set sizes are different'));
                return false;
            }
            const value1Arr = [...value1].sort();
            const value2Arr = [...value2].sort();
            if (!await TestingHelper.compare(value1Arr, value2Arr)) {
                TestingHelper.logDebug(Util.format('compare: sets are different: %s and %s',
                    TestingHelper.printValue(value1Arr), TestingHelper.printValue(value2Arr)));
                return false;
            }
            return true;
        }
        else if (value2 instanceof BinaryObject) {
            if (value1 instanceof BinaryObject) {
                if (value1.getTypeName() !== value2.getTypeName()) {
                    TestingHelper.logDebug(Util.format('compare: binary object type names are different'));
                    return false;
                }
                if (!await TestingHelper.compare(value1.getFieldNames(), value2.getFieldNames())) {
                    TestingHelper.logDebug(Util.format('compare: binary object field names are different'));
                    return false;
                }
                for (let fieldName of value1.getFieldNames()) {
                    if (!value1.hasField(fieldName) || !value2.hasField(fieldName) ||
                        !await TestingHelper.compare(await value1.getField(fieldName), await value1.getField(fieldName))) {
                        TestingHelper.logDebug(Util.format('compare: binary objects field "%s" values are different', fieldName));
                        return false;
                    }
                }
                return true;
            }
            else {
                let value;
                for (let key of Object.keys(value1)) {
                    value = await value2.getField(key);
                    if (!(await TestingHelper.compare(value1[key], value))) {
                        TestingHelper.logDebug(Util.format('compare: binary object values for key %s are different: %s and %s',
                            TestingHelper.printValue(key), TestingHelper.printValue(value1[key]), TestingHelper.printValue(value)));
                        return false;
                    }
                }
                return true;
            }
        }
        else {
            for (let key of Object.keys(value1)) {
                if (!(await TestingHelper.compare(value1[key], value2[key]))) {
                    TestingHelper.logDebug(Util.format('compare: object values for key %s are different: %s and %s',
                        TestingHelper.printValue(key), TestingHelper.printValue(value1[key]), TestingHelper.printValue(value2[key])));
                    return false;
                }
            }
            return true;
        }
    }
}

module.exports = TestingHelper;
