"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const deepmerge_1 = __importDefault(require("deepmerge"));
const logger_1 = __importDefault(require("@wdio/logger"));
const RequireLibrary_1 = __importDefault(require("./RequireLibrary"));
const FileSystemPathService_1 = __importDefault(require("./FileSystemPathService"));
const utils_1 = require("../utils");
const constants_1 = require("../constants");
const constants_2 = require("../constants");
const log = (0, logger_1.default)('@wdio/config:ConfigParser');
const MERGE_OPTIONS = { clone: false };
class ConfigParser {
    constructor(_pathService = new FileSystemPathService_1.default(), _moduleRequireService = new RequireLibrary_1.default()) {
        this._pathService = _pathService;
        this._moduleRequireService = _moduleRequireService;
        this._config = (0, constants_2.DEFAULT_CONFIGS)();
        this._capabilities = [];
    }
    autoCompile() {
        /**
         * on launcher compile files if Babel or TypeScript are installed using our defaults
         */
        if (this._config.autoCompileOpts && !(0, utils_1.loadAutoCompilers)(this._config.autoCompileOpts, this._moduleRequireService)) {
            log.debug('No compiler found, continue without compiling files');
        }
    }
    /**
     * merges config file with default values
     * @param {String} filename path of file relative to current directory
     */
    addConfigFile(filename) {
        if (typeof filename !== 'string') {
            throw new Error('addConfigFile requires filepath');
        }
        const filePath = this._pathService.ensureAbsolutePath(filename);
        try {
            const config = this._pathService.loadFile(filePath).config;
            if (typeof config !== 'object') {
                throw new Error('addConfigEntry requires config key');
            }
            /**
             * clone the original config
             */
            const fileConfig = (0, deepmerge_1.default)(config, {}, MERGE_OPTIONS);
            /**
             * merge capabilities
             */
            const defaultTo = Array.isArray(this._capabilities) ? [] : {};
            this._capabilities = (0, deepmerge_1.default)(this._capabilities, fileConfig.capabilities || defaultTo, MERGE_OPTIONS);
            delete fileConfig.capabilities;
            /**
             * Add hooks from the file config and remove them from file config object to avoid
             * complications when using merge function
             */
            this.addService(fileConfig);
            for (let hookName of constants_1.SUPPORTED_HOOKS) {
                delete fileConfig[hookName];
            }
            this._config = (0, deepmerge_1.default)(this._config, fileConfig, MERGE_OPTIONS);
            /**
             * remove `watch` from config as far as it can be only passed as command line argument
             */
            delete this._config.watch;
        }
        catch (e) {
            log.error(`Failed loading configuration file: ${filePath}:`, e.message);
            throw e;
        }
    }
    /**
     * merge external object with config object
     * @param  {Object} object  desired object to merge into the config object
     */
    merge(object = {}) {
        const spec = Array.isArray(object.spec) ? object.spec : [];
        const exclude = Array.isArray(object.exclude) ? object.exclude : [];
        this._config = (0, deepmerge_1.default)(this._config, object, MERGE_OPTIONS);
        /**
         * overwrite config specs that got piped into the wdio command
         */
        if (object.specs && object.specs.length > 0) {
            this._config.specs = object.specs;
        }
        else if (object.exclude && object.exclude.length > 0) {
            this._config.exclude = object.exclude;
        }
        /**
         * overwrite capabilities
         */
        this._capabilities = (0, utils_1.validObjectOrArray)(this._config.capabilities) ? this._config.capabilities : this._capabilities;
        /**
         * save original specs if Cucumber's feature line number is provided
         */
        if (this._config.spec && (0, utils_1.isCucumberFeatureWithLineNumber)(this._config.spec)) {
            /**
             * `this._config.spec` is string instead of Array in watch mode
             */
            this._config.cucumberFeaturesWithLineNumbers = Array.isArray(this._config.spec) ? [...this._config.spec] : [this._config.spec];
        }
        /**
         * run single spec file only, regardless of multiple-spec specification
         */
        if (spec.length > 0) {
            this._config.specs = this.setFilePathToFilterOptions(spec, this._config.specs);
        }
        if (exclude.length > 0) {
            this._config.exclude = this.setFilePathToFilterOptions(exclude, this._config.exclude);
        }
    }
    /**
     * Add hooks from an existing service to the runner config.
     * @param {Object} service - an object that contains hook methods.
     */
    addService(service) {
        const addHook = (hookName, hook) => {
            // @ts-ignore Expression produces a union type that is too complex to represent
            const existingHooks = this._config[hookName];
            if (!existingHooks) {
                // @ts-ignore Expression produces a union type that is too complex to represent
                this._config[hookName] = hook.bind(service);
            }
            else if (typeof existingHooks === 'function') {
                // @ts-ignore Expression produces a union type that is too complex to represent
                this._config[hookName] = [existingHooks, hook.bind(service)];
            }
            else {
                // @ts-ignore Expression produces a union type that is too complex to represent
                this._config[hookName] = [...existingHooks, hook.bind(service)];
            }
        };
        for (const hookName of constants_1.SUPPORTED_HOOKS) {
            const hooksToBeAdded = service[hookName];
            if (!hooksToBeAdded) {
                continue;
            }
            if (typeof hooksToBeAdded === 'function') {
                addHook(hookName, hooksToBeAdded);
            }
            else if (Array.isArray(hooksToBeAdded)) {
                for (const hookToAdd of hooksToBeAdded) {
                    if (typeof hookToAdd === 'function') {
                        addHook(hookName, hookToAdd);
                    }
                }
            }
        }
    }
    /**
     * get excluded files from config pattern
     */
    getSpecs(capSpecs, capExclude) {
        var _a;
        let specs = ConfigParser.getFilePaths(this._config.specs, undefined, this._pathService);
        let spec = Array.isArray(this._config.spec) ? this._config.spec : [];
        let exclude = ConfigParser.getFilePaths(this._config.exclude, undefined, this._pathService);
        let suites = Array.isArray(this._config.suite) ? this._config.suite : [];
        /**
         * check if user has specified a specific suites to run
         */
        if (suites.length > 0) {
            let suiteSpecs = [];
            for (let suiteName of suites) {
                let suite = (_a = this._config.suites) === null || _a === void 0 ? void 0 : _a[suiteName];
                if (!suite) {
                    log.warn(`No suite was found with name "${suiteName}"`);
                }
                if (Array.isArray(suite)) {
                    suiteSpecs = suiteSpecs.concat(ConfigParser.getFilePaths(suite, undefined, this._pathService));
                }
            }
            if (suiteSpecs.length === 0) {
                throw new Error(`The suite(s) "${suites.join('", "')}" you specified don't exist ` +
                    'in your config file or doesn\'t contain any files!');
            }
            // Allow --suite and --spec to both be defined on the command line
            // Removing any duplicate tests that could be included
            let tmpSpecs = spec.length > 0 ? [...specs, ...suiteSpecs] : suiteSpecs;
            //Only merge capability specs if --spec is not defined
            if (spec.length === 0) {
                if (Array.isArray(capSpecs)) {
                    tmpSpecs = ConfigParser.getFilePaths(capSpecs, undefined, this._pathService);
                }
                if (Array.isArray(capExclude)) {
                    exclude = ConfigParser.getFilePaths(capExclude, undefined, this._pathService);
                }
            }
            specs = [...new Set(tmpSpecs)];
            return this.filterSpecs(specs, exclude);
        }
        //Only merge capability specs if --spec is not defined
        if (spec.length === 0) {
            if (Array.isArray(capSpecs)) {
                specs = ConfigParser.getFilePaths(capSpecs, undefined, this._pathService);
            }
            if (Array.isArray(capExclude)) {
                exclude = ConfigParser.getFilePaths(capExclude, undefined, this._pathService);
            }
        }
        return this.filterSpecs(specs, exclude);
    }
    /**
     * sets config attribute with file paths from filtering
     * options from cli argument
     *
     * @param  {String[]} cliArgFileList  list of files in a string form
     * @param  {Object} config  config object that stores the spec and exclude attributes
     * cli argument
     * @return {String[]} List of files that should be included or excluded
     */
    setFilePathToFilterOptions(cliArgFileList, config) {
        const filesToFilter = new Set();
        const fileList = ConfigParser.getFilePaths(config, undefined, this._pathService);
        cliArgFileList.forEach(filteredFile => {
            filteredFile = (0, utils_1.removeLineNumbers)(filteredFile);
            // Send single file/file glob to getFilePaths - not supporting hierarchy in spec/exclude
            // Return value will alwyas be string[]
            let globMatchedFiles = ConfigParser.getFilePaths(this._pathService.glob(filteredFile), undefined, this._pathService);
            if (this._pathService.isFile(filteredFile)) {
                filesToFilter.add(this._pathService.ensureAbsolutePath(filteredFile));
            }
            else if (globMatchedFiles.length) {
                globMatchedFiles.forEach(file => filesToFilter.add(file));
            }
            else {
                // fileList can be a string[] or a string[][]
                fileList.forEach(file => {
                    if (typeof file === 'string') {
                        if (file.match(filteredFile)) {
                            filesToFilter.add(file);
                        }
                    }
                    else if (Array.isArray(file)) {
                        file.forEach(subFile => {
                            if (subFile.match(filteredFile)) {
                                filesToFilter.add(subFile);
                            }
                        });
                    }
                    else {
                        log.warn('Unexpected entry in specs that is neither string nor array: ', file);
                    }
                });
            }
        });
        if (filesToFilter.size === 0) {
            throw new Error(`spec file(s) ${cliArgFileList.join(', ')} not found`);
        }
        return [...filesToFilter];
    }
    /**
     * return configs
     */
    getConfig() {
        return this._config;
    }
    /**
     * return capabilities
     */
    getCapabilities(i) {
        if (typeof i === 'number' && Array.isArray(this._capabilities) && this._capabilities[i]) {
            return this._capabilities[i];
        }
        return this._capabilities;
    }
    /**
     * returns a flattened list of globbed files
     *
     * @param  {String[] | String[][]} filenames list of files to glob
     * @param  {Boolean} flag to indicate omission of warnings
     * @param  {FileSystemPathService} file system path service for expanding globbed file names
     * @param  {number} hierarchy depth to prevent recursive calling beyond a depth of 1
     * @return {String[] | String[][]} list of files
     */
    static getFilePaths(patterns, omitWarnings, findAndGlob = new FileSystemPathService_1.default(), hierarchyDepth) {
        let files = [];
        let groupedFiles = [];
        if (typeof patterns === 'string') {
            patterns = [patterns];
        }
        // patterns must be an array of strings and/or string arrays
        // check and throw and error if not
        if (!Array.isArray(patterns)) {
            throw new Error('specs or exclude property should be an array of strings, specs may also be an array of string arrays');
        }
        patterns = patterns.map(pattern => {
            if (Array.isArray(pattern)) {
                return pattern.map(subPattern => (0, utils_1.removeLineNumbers)(subPattern));
            }
            return (0, utils_1.removeLineNumbers)(pattern);
        });
        for (let pattern of patterns) {
            // If pattern is an array, then call getFilePaths again
            // But only call one level deep, can't have multiple levels of hierarchy
            if (Array.isArray(pattern) && !hierarchyDepth) {
                // Will always only get a string array back
                groupedFiles = ConfigParser.getFilePaths(pattern, omitWarnings, findAndGlob, 1);
                files.push(groupedFiles);
            }
            else if (Array.isArray(pattern) && hierarchyDepth) {
                log.error('Unexpected depth of hierarchical arrays');
            }
            else {
                pattern = pattern.toString().replace(/\\/g, '/');
                let filenames = findAndGlob.glob(pattern);
                filenames = filenames.filter((filename) => constants_1.SUPPORTED_FILE_EXTENSIONS.find((ext) => filename.endsWith(ext)));
                filenames = filenames.map(filename => findAndGlob.ensureAbsolutePath(filename));
                if (filenames.length === 0 && !omitWarnings) {
                    log.warn('pattern', pattern, 'did not match any file');
                }
                files = (0, deepmerge_1.default)(files, filenames, MERGE_OPTIONS);
            }
        }
        return files;
    }
    /**
     * returns specs files with the excludes filtered
     *
     * @param  {String[] | String[][]} spec files -  list of spec files
     * @param  {String[]} exclude files -  list of exclude files
     * @return {String[] | String[][]} list of spec files with excludes removed
     */
    filterSpecs(specs, exclude) {
        return specs.reduce((returnVal, spec) => {
            if (Array.isArray(spec)) {
                returnVal.push(spec.filter(specItem => !exclude.includes(specItem)));
            }
            else if (exclude.indexOf(spec) === -1) {
                returnVal.push(spec);
            }
            return returnVal;
        }, []);
    }
}
exports.default = ConfigParser;
