/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

'use strict';

/**
 * This script runs instrumentation tests one by one with retries
 * Instrumentation tests tend to be flaky, so rerunning them individually increases
 * chances for success and reduces total average execution time.
 *
 * We assume that all instrumentation tests are flat in one folder
 * Available arguments:
 * --path - path to all .java files with tests
 * --package - com.facebook.react.tests
 * --retries [num] - how many times to retry possible flaky commands: npm install and running tests, default 1
 */

const argv = require('yargs').argv;
const async = require('async');
const child_process = require('child_process');
const fs = require('fs');
const path = require('path');

const colors = {
    GREEN: '\x1b[32m',
    RED: '\x1b[31m',
    RESET: '\x1b[0m',
};

const test_opts = {
    FILTER: new RegExp(argv.filter || '.*', 'i'),
    IGNORE: argv.ignore || null,
    PACKAGE: argv.package || 'com.facebook.react.tests',
    PATH: argv.path || './ReactAndroid/src/androidTest/java/com/facebook/react/tests',
    RETRIES: parseInt(argv.retries || 2, 10),

    TEST_TIMEOUT: parseInt(argv['test-timeout'] || 1000 * 60 * 10, 10),

    OFFSET: argv.offset,
    COUNT: argv.count,
};

let max_test_class_length = Number.NEGATIVE_INFINITY;

let testClasses = fs.readdirSync(path.resolve(process.cwd(), test_opts.PATH))
    .filter((file) => {
        return file.endsWith('.java');
    }).map((clazz) => {
        return path.basename(clazz, '.java');
    });

if (test_opts.IGNORE) {
    test_opts.IGNORE = new RegExp(test_opts.IGNORE, 'i');
    testClasses = testClasses.filter(className => {
        return !test_opts.IGNORE.test(className);
    });
}

testClasses = testClasses.map((clazz) => {
    return test_opts.PACKAGE + '.' + clazz;
}).filter((clazz) => {
    return test_opts.FILTER.test(clazz);
});

// only process subset of the tests at corresponding offset and count if args provided
if (test_opts.COUNT != null && test_opts.OFFSET != null) {
    const start = test_opts.COUNT * test_opts.OFFSET;
    const end = start + test_opts.COUNT;

    if (start >= testClasses.length) {
        testClasses = [];
    } else if (end >= testClasses.length) {
        testClasses = testClasses.slice(start);
    } else {
        testClasses = testClasses.slice(start, end);
    }
}

async.mapSeries(testClasses, (clazz, callback) => {
    if (clazz.length > max_test_class_length) {
        max_test_class_length = clazz.length;
    }

    return async.retry(test_opts.RETRIES, (retryCb) => {
        const test_process = child_process.spawn('./.circleci/Dockerfiles/scripts/run-instrumentation-tests-via-adb-shell.sh', [test_opts.PACKAGE, clazz], {
            stdio: 'inherit',
        });

        const timeout = setTimeout(() => {
            test_process.kill();
        }, test_opts.TEST_TIMEOUT);

        test_process.on('error', (err) => {
            clearTimeout(timeout);
            retryCb(err);
        });

        test_process.on('exit', (code) => {
            clearTimeout(timeout);

            if (code !== 0) {
                return retryCb(new Error(`Process exited with code: ${code}`));
            }

            return retryCb();
        });
    }, (err) => {
        return callback(null, {
            name: clazz,
            status: err ? 'failure' : 'success',
        });
    });
}, (err, results) => {
    print_test_suite_results(results);

    const failures = results.filter((test) => {
        return test.status === 'failure';
    });

    return failures.length === 0 ? process.exit(0) : process.exit(1);
});

function print_test_suite_results(results) {
    console.log('\n\nTest Suite Results:\n');

    let color;
    let failing_suites = 0;
    let passing_suites = 0;

    function pad_output(num_chars) {
        let i = 0;

        while (i < num_chars) {
            process.stdout.write(' ');
            i++;
        }
    }
    results.forEach((test) => {
        if (test.status === 'success') {
            color = colors.GREEN;
            passing_suites++;
        } else if (test.status === 'failure') {
            color = colors.RED;
            failing_suites++;
        }

        process.stdout.write(color);
        process.stdout.write(test.name);
        pad_output((max_test_class_length - test.name.length) + 8);
        process.stdout.write(test.status);
        process.stdout.write(`${colors.RESET}\n`);
    });

    console.log(`\n${passing_suites} passing, ${failing_suites} failing!`);
}
