/**
 * testsuite.js
 * Copyright (c) 2000 - 2017 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jonghwan Park <iwin100.park@samsung.com>
 * Kitae Kim <kt920.kim@samsung.com>
 *
 * Licensed 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.
 *
 * Contributors:
 * - S-Core Co., Ltd
**/

/**
 * Tizen git project plugin
 * @module models/tizen-project/git-project
 */

var async = require('async');
var path = require('path');
var fs = require('fs');
var _ = require('underscore');
var os = require('os');

var DError = require('../../core/exception');
var dibs = require('../../core/dibs');
var Job = require('../dibs.model.common/job.js');
var Tutils = require('../org.tizen.common/tizen_utils');
var utils = require('../../lib/utils');
var Process = require('../dibs.core/process.js');
var TizenCommon = require('../org.tizen.common/tizen_common.js');
var Installer = require('../org.tizen.projects/installer.js');
var TizenGit = require('../org.tizen.projects/git-control.js');
var RemoteRepo = require('../org.tizen.repository/remote-repo.js');
var Record = require('./lib/record.js');


module.exports.createJob = createJob;
module.exports.initializeJob = initializeJob;
module.exports.executeJob = executeJob;
module.exports.checkMutualExclusiveJob = TizenCommon.checkMutualExclusiveJob;


/**
 * Tizen git job that is specialized on tizen git project
 * @constructor
 * @augments module:models/job
 * @param {module:models/job~Job} baseJob - base job object
 */

function TizenTestsuiteJob(baseJob) {
    Job.copy(this, baseJob);

    this.gitRepo = null;
    this.gitCommit = null;
    this.snapshot = null;

    // NOTE. Initializing on windows is very slow
    this.initEnvironments = dibs.projectTypes['Tizen-Testsuite'].environments
        .filter(function (e) {
            return (e.indexOf('windows') === -1);
        });
}


function createJob(userEmail, distName, prjName, prjType, environmentName, parentId, distType, options, callback) {
    Job.create(userEmail, distName, distType, prjName, prjType, environmentName, parentId, null, options,
        function (err, newJob) {
            callback(err, new TizenTestsuiteJob(newJob));
        });
}


function initializeJob(job, options, callback) {
    var monitor = options.monitor;

    async.waterfall([
        // get test source code
        function (cb) {
            monitor.updateProgress('Preparing source code... ' + job.options.GIT_REPO);
            TizenGit.getSourceCode(job, options, cb);
        },
        // check testsuite dir
        function (srcPathInfo, cb) {
            monitor.updateProgress('Preparing source code succeeded!');
            monitor.updateProgress('Checking testsuites...');
            checkTestsuiteSource(job.options.TESTSUITE_NAME, job.environmentName, srcPathInfo, cb);
        },
        function (scriptFile, cb) {
            monitor.updateProgress(' - ' + path.basename(scriptFile), cb);
        },
        function (cb) {
            monitor.updateProgress('Checking test server information...');
            job.execEnvironments = [
                utils.getEnvironmentIdFromName(job.environmentName, job.projectType)
            ];
            if (isServerPrepared(dibs.getAllServers(), job.execEnvironments)) {
                cb(null);
            } else {
                cb(new DError('TIZENJOB010', {
                    envName: job.execEnvironments.join(',')
                }));
            }
        }
    ], function (err) {
        callback(err, null);
    });
}


function executeJob(job, options, callback) {
    var jobWorkPath = options.jobWorkPath;
    var monitor = options.monitor;
    var server = dibs.getServer(options.parentServerId);

    var srcPath = path.join(jobWorkPath, 'source');
    var repo = null;
    var recordStarted = false;
    var videoFile = null;
    var recordingProc = null;

    async.waterfall([
        // get latest snapshots of repository
        function (cb) {
            monitor.updateProgress('Getting the latest snapshot of repository ...');
            if (job.options.TEST_DIST_BASE_URL) {
                repo = RemoteRepo.createRemoteRepo(job.options.TEST_DIST_BASE_URL,
                    {
                        distName: job.options.TEST_DIST_NAME,
                        snapshotName: null
                    });
                repo.open(null, cb);
            } else {
                repo = dibs.getServersByType('repo')[0];
                cb(null);
            }
        },
        function (cb) {
            repo.searchSnapshots({
                name: null,
                repoType: 'tizen',
                distName: job.options.TEST_DIST_NAME
            }, function (err, snapshots) {
                if (!err && snapshots.length > 0) {
                    job.snapshot = snapshots[0];
                    monitor.updateProgress(' - ' + job.snapshot.name);
                    job.snapshot.distName = job.options.TEST_DIST_NAME;
                }
                cb(err);
            });
        },
        function (cb) {
            monitor.updateProgress('Checking initialization server matched...', cb);
        },
        // check
        function (cb) {
            // NOTE.If exec-server is different from init-server,
            //      get source code again
            if (job.initServerId !== server.id) {
                monitor.updateProgress(' - exec server is different from init server');
                monitor.updateProgress(' - preparing source code...' + job.options.GIT_REPO);
                TizenGit.getSourceCode(job, options, function (err) {
                    cb(err);
                });
            } else {
                monitor.updateProgress(' - exec server is same as init server');
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress('Testing...', cb);
        },
        function (cb) {
            killSDKProcesses(monitor, cb);
        },
        function (cb) {
            if (job.options.TEST_VIDEO_RECORD) {
                monitor.updateProgress('Video recording...');
                Record.startRecord(jobWorkPath, job.options.TESTSUITE_NAME + '_' + utils.generateTimeStamp(), function (err, process, filePath) {
                    if (!err) {
                        recordStarted = true;
                        recordingProc = process;
                        videoFile = filePath;
                    }
                    cb(err);
                });
            } else {
                cb(null);
            }
        },
        function (cb) {
            executeTestsuite(job, options, repo, cb);
        },
        function (cb) {
            monitor.updateProgress('Testing done!');
            parseTestResultFile(job.options.TESTSUITE_NAME,
                srcPath, job.options.TEST_RESULT_FORMAT, monitor, function (err, result) {

                    if (!err) {
                        job.testResult = result;
                    }
                    cb(err);
                });
        },
        function (cb) {
            monitor.updateProgress('Terminating job...', cb);
        },
        // save test result
        function (cb) {
            job.options.TEST_RESULT = job.testResult;
            cb(null);
        }
    ], function (err) {
        if (recordStarted) {
            monitor.updateProgress('Video encording...');
            Record.terminate(recordingProc, monitor.updateProgress, function (err1) {
                if (err1) {
                    monitor.updateProgress({
                        log: err1.message,
                        logType: 'warn'
                    });
                    callback(err, job);
                } else {
                    monitor.updateProgress('Uploading video...');
                    var taRepo = dibs.getServersByType('repo')[0];
                    taRepo.registerPackages([videoFile],
                        {
                            repoType: 'tizen-ta',
                            distName: job.distName
                        },
                        function (err2) {
                            if (err2) {
                                monitor.updateProgress({
                                    log: err2.message,
                                    logType: 'warn'
                                });
                            } else {
                                monitor.updateProgress('Uploading video succeeded!');
                                job.options.RECORD_FILE_NAME = path.basename(videoFile);
                            }
                            callback(err, job);
                        });
                }
            });
        } else {
            callback(err, job);
        }
    });
}


function checkTestsuiteSource(testsuiteName, envName, srcPathDir, callback) {
    var testsuiteDir;
    async.waterfall([
        function (cb) {
            var exists = fs.existsSync(path.join(srcPathDir, 'testsuites'));
            if (exists) {
                testsuiteDir = path.join(srcPathDir, 'testsuites');
                cb(null);
            } else {
                cb(new DError('TIZENTA003', {
                    path: testsuiteDir
                }));
            }
        },
        function (cb) {
            var exists = fs.existsSync(path.join(srcPathDir, 'testsuites', testsuiteName));
            if (exists) {
                testsuiteDir = path.join(srcPathDir, 'testsuites', testsuiteName);
            }
            findTestScriptFile(envName, testsuiteDir, cb);
        }
    ], callback);
}


function findTestScriptFile(osName, testsuiteDir, callback) {
    var candidates = [];
    var scriptType = 'test';

    // check os name
    var ext = Tutils.getOSCategory(osName) === 'windows' ? 'BAT' : 'sh';
    candidates.push(path.join(testsuiteDir, scriptType + '.' + osName + '.' + ext));
    candidates.push(path.join(testsuiteDir, scriptType + '.' + osName));

    // check osCategory
    var osCategory = Tutils.getOSCategory(osName);
    candidates.push(path.join(testsuiteDir, scriptType + '.' + osCategory + '.' + ext));
    candidates.push(path.join(testsuiteDir, scriptType + '.' + osCategory));

    // get existing files
    async.detectSeries(candidates, fs.exists,
        function (script) {
            if (script) {
                callback(null, script);
            } else {
                callback(new DError('TIZENTA004', {
                    scriptType: scriptType
                }), null);
            }
        });
}


function isServerPrepared(servers, envNames) {
    return (_.select(servers, function (svr) {
        return (svr.type === 'builder' &&
            svr.status === 'RUNNING' &&
            envNames &&
            _.intersection(envNames, _.map(svr.environments, function (e) {
                return e.id;
            })).length > 0);
    }).length > 0);
}


function executeTestsuite(job, options, repo, callback) {
    var jobWorkPath = options.jobWorkPath;
    var monitor = options.monitor;
    var workspaceTempDir = options.workSpaceTempPath;

    var srcPath = path.join(jobWorkPath, 'source');
    var host = Tutils.getTargetOS(os.platform(), os.arch());
    var buildRoot = path.join(jobWorkPath, 'buildRoot');

    var testTimeout = null;
    if (job.options.TEST_TIMEOUT) {
        testTimeout = setTimeout(function () {
            monitor.cancel(callback);
        }, job.options.TEST_TIMEOUT);
    }

    async.waterfall([
        // execute system pre-process
        function (cb) {
            monitor.updateProgress('Loading pre-defined test evironment...');
            setupPredefinedEnvironment(
                job.options.TEST_PREDEF_ENV,
                job.options.TEST_DIST_NAME,
                job.options.TEST_DIST_BASE_URL,
                jobWorkPath,
                dibs.thisServer.config.get('sudo_password'),
                job.distName,
                monitor, cb);
        },
        function (cb) {
            monitor.updateProgress('Loading test environment done!', cb);
        },
        // download/install pre-requiste packages
        function (cb) {
            monitor.updateProgress('Installing pre-requisite packages... ');

            var depPkgs = [];
            if (job.options.TEST_PREREQ_PKGS) {
                var pkgNames = job.options.TEST_PREREQ_PKGS.split(',');
                for (var i = 0; i < pkgNames.length; i++) {
                    var pkgName = pkgNames[i];

                    if (job.snapshot && job.snapshot.osPackages &&
                        job.snapshot.osPackages[host.os] &&
                        job.snapshot.osPackages[host.os][pkgName]) {

                        var pkg = job.snapshot.osPackages[host.os][pkgName];
                        pkg.os = host.os;
                        depPkgs.push(pkg);
                    } else {
                        return cb(new DError('TIZENTA011', {
                            pkgName: pkgName
                        }));
                    }
                }
            }
            Installer.installMultiplePackages(depPkgs,
                buildRoot, repo, job.options.TEST_DIST_NAME, host,
                {
                    clean: true,
                    extractCacheDir: workspaceTempDir
                }, monitor, cb);
        },
        function (cb) {
            monitor.updateProgress(' - installing pre-requisites done!', cb);
        },
        function (cb) {
            var env = process.env;
            env.TESTSUITE_NAME = job.options.TESTSUITE_NAME;
            env.TARGET_OS = job.environmentName;
            env.SRCDIR = srcPath;
            env.ROOTDIR = buildRoot;
            env.TEST_DIST_BASE_URL = job.options.TEST_DIST_BASE_URL;
            env.TEST_DIST_NAME = job.options.TEST_DIST_NAME;
            env.TEST_SUDO_PASSWORD = dibs.thisServer.config.get('sudo_password');
            env.TEST_SIKULI_EXEC_PATH = dibs.thisServer.config.get('sikuli_exec_path');

            executeTestScript(job.options.TESTSUITE_NAME, job.environmentName,
                srcPath, env, monitor, cb);
        }
    ], function (err) {
        // clear timeout
        if (testTimeout) {
            clearTimeout(testTimeout);
        }

        callback(err);
    });
}


function setupPredefinedEnvironment(environmentId, distName, distBaseURL, jobWorkPath, passwd, testDistName, monitor, callback) {
    if (!environmentId) {
        environmentId = 'tizen.none.env';
    }

    var exts = dibs.plugin.getExtensions('org.tizen.test-automation.predefined.environment');
    for (var i = 0; i < exts.length; i++) {
        var ext = exts[i];
        if (environmentId === ext.attributes.id) {
            ext.module.setup(environmentId, distName, distBaseURL, jobWorkPath, passwd, testDistName, monitor, callback);
            return;
        }
    }

    callback(new DError('TIZENTA009', {
        envName: environmentId
    }));
}


function executeTestScript(testsuiteName, osName, srcPath, env, monitor, callback) {
    var scriptFile = null;
    async.waterfall([
        function (cb) {
            monitor.updateProgress('Get script dir... ');
            var isSuiteDirExists = fs.existsSync(path.join(srcPath, 'testsuites', testsuiteName));
            var testsuiteDir;
            if (isSuiteDirExists) {
                testsuiteDir = path.join(srcPath, 'testsuites', testsuiteName);
            } else {
                testsuiteDir = path.join(srcPath, 'testsuites');
            }
            monitor.updateProgress('script dir: ' + testsuiteDir);
            findTestScriptFile(osName, testsuiteDir, cb);
        },
        // executing setup script
        function (file, cb) {
            scriptFile = file;
            monitor.updateProgress('Executing setup script... ');
            runScript(scriptFile, 'setup', srcPath, env, monitor, cb);
        },
        // executing scripts
        function (cb) {
            monitor.updateProgress('Executing test script... ');
            runScript(scriptFile, 'test', srcPath, env, monitor, cb);
        }
    ], function (err) {
        if (err) {
            return callback(err);
        }
        monitor.updateProgress('Executing teardown script... ');
        runScript(scriptFile, 'teardown', srcPath, env, monitor, callback);
    });
}


function runScript(script, target, workdir, env, monitor, callback) {
    var runFile = path.join(workdir,
        utils.generateTimeStamp(true) + '.' + path.basename(script));
    async.waterfall([
        //gen script
        function (cb) {
            monitor.updateProgress(' - reading script file...');
            fs.readFile(script, cb);
        },
        function (scriptBuffer, cb) {
            monitor.updateProgress(' - generate script file...');

            var scriptString = null;
            if (os.platform() === 'win32') {
                scriptString = scriptBuffer.toString().replace(/[^}]*$/, '') + os.EOL +
                    'CALL ' + target + os.EOL;
            } else {
                scriptString = '#!/bin/bash -xe' + os.EOL +
                    scriptBuffer.toString().replace(/[^}]*$/, '') + os.EOL +
                    target + os.EOL +
                    'echo "success"' + os.EOL;
            }

            fs.writeFileSync(runFile, scriptString, {
                mode: 493
            });
            cb(null);
        },
        // run
        function (cb) {
            monitor.updateProgress(' - executing ' + path.basename(runFile) + ' ...', cb);
        },
        function (cb) {
            executeScript(runFile, workdir, env, monitor, cb);
        }
    ], function (err) {
        utils.removePathIfExist(runFile, function () {
            callback(err);
        });
    });
}


function executeScript(script, workdir, env, monitor, callback) {
    async.waterfall([
        function (wcb) {
            fs.chmod(script, '0777', wcb);
        },
        function (wcb) {

            var run = Process.create(script,
                [],
                {
                    cwd: workdir,
                    env: env
                },
                {
                    onStdout: function (line) {
                        monitor.updateProgress('   ' + line);
                    },
                    onStderr: function (line) {
                        monitor.updateProgress({
                            log: '   ' + line,
                            logType: 'error'
                        });
                    },
                    onExit: function (code) {
                        if (code !== 0) {
                            var error = new Error('Executing file(' + script + ') process exited with code ' + code);
                            wcb(error);
                        } else {
                            wcb(null);
                        }
                    }
                });

            // add process to monitor
            monitor.addProcess(run);
        }
    ], callback);
}


function parseTestResultFile(testsuiteName, srcPathDir, resultFormat, monitor, callback) {
    var exts = dibs.plugin.getExtensions('org.tizen.test-automation.result.format');
    for (var i = 0; i < exts.length; i++) {
        var ext = exts[i];
        if (resultFormat === ext.attributes.id) {
            ext.module.parse(testsuiteName, srcPathDir, monitor, callback);
            return;
        }
    }

    callback(new DError('TIZENTA010', {
        format: resultFormat
    }), null);
}


function killSDKProcesses(monitor, callback) {
    async.series([
        function (cb) {
            killall('java', monitor, cb);
        },
        function (cb) {
            killall('recordmydesktop', monitor, cb);
        },
        function (cb) {
            killall('gksudo', monitor, cb);
        }
    ], function (err) {
        callback(err);
    });
}


function killall(procName, monitor, callback) {
    Process.create('killall',
        ['-9', procName],
        {},
        {
            onStdout: function (line) {
                monitor.updateProgress('   ' + line);
            },
            onStderr: function (line) {
                monitor.updateProgress({
                    log: '   ' + line,
                    logType: 'warn'
                });
            },
            onExit: function () {
                callback(null);
            }
        });
}

