/**
 * Copyright 2016-2020, Optimizely
 *
 * 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.
 */
import sinon from 'sinon';
import { assert, expect, config } from 'chai';
import { forEach, cloneDeep } from 'lodash';
import { getLogger } from '@optimizely/js-sdk-logging';
import { sprintf } from '@optimizely/js-sdk-utils';

import fns from '../../utils/fns';
import projectConfig from './';
import {
  ERROR_MESSAGES,
  FEATURE_VARIABLE_TYPES,
  LOG_LEVEL,
} from '../../utils/enums';
import loggerPlugin from '../../plugins/logger';
import testDatafile from '../../tests/test_data';
import configValidator from '../../utils/config_validator';

var logger = getLogger();

describe('lib/core/project_config', function() {
  describe('createProjectConfig method', function() {
    it('should set properties correctly when createProjectConfig is called', function() {
      var testData = testDatafile.getTestProjectConfig();
      var configObj = projectConfig.createProjectConfig(testData);

      forEach(testData.audiences, function(audience) {
        audience.conditions = JSON.parse(audience.conditions);
      });

      assert.strictEqual(configObj.accountId, testData.accountId);
      assert.strictEqual(configObj.projectId, testData.projectId);
      assert.strictEqual(configObj.revision, testData.revision);
      assert.deepEqual(configObj.events, testData.events);
      assert.deepEqual(configObj.audiences, testData.audiences);
      testData.groups.forEach(function(group) {
        group.experiments.forEach(function(experiment) {
          experiment.groupId = group.id;
          experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key');
        });
      });
      assert.deepEqual(configObj.groups, testData.groups);

      var expectedGroupIdMap = {
        666: testData.groups[0],
        667: testData.groups[1],
      };

      assert.deepEqual(configObj.groupIdMap, expectedGroupIdMap);

      var expectedExperiments = testData.experiments;
      forEach(configObj.groupIdMap, function(group, Id) {
        forEach(group.experiments, function(experiment) {
          experiment.groupId = Id;
          expectedExperiments.push(experiment);
        });
      });

      forEach(expectedExperiments, function(experiment) {
        experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key');
      });

      assert.deepEqual(configObj.experiments, expectedExperiments);

      var expectedAttributeKeyMap = {
        browser_type: testData.attributes[0],
        boolean_key: testData.attributes[1],
        integer_key: testData.attributes[2],
        double_key: testData.attributes[3],
        valid_positive_number: testData.attributes[4],
        valid_negative_number: testData.attributes[5],
        invalid_number: testData.attributes[6],
        array: testData.attributes[7],
      };

      assert.deepEqual(configObj.attributeKeyMap, expectedAttributeKeyMap);

      var expectedExperimentKeyMap = {
        testExperiment: configObj.experiments[0],
        testExperimentWithAudiences: configObj.experiments[1],
        testExperimentNotRunning: configObj.experiments[2],
        testExperimentLaunched: configObj.experiments[3],
        groupExperiment1: configObj.experiments[4],
        groupExperiment2: configObj.experiments[5],
        overlappingGroupExperiment1: configObj.experiments[6],
      };

      assert.deepEqual(configObj.experimentKeyMap, expectedExperimentKeyMap);

      var expectedEventKeyMap = {
        testEvent: testData.events[0],
        'Total Revenue': testData.events[1],
        testEventWithAudiences: testData.events[2],
        testEventWithoutExperiments: testData.events[3],
        testEventWithExperimentNotRunning: testData.events[4],
        testEventWithMultipleExperiments: testData.events[5],
        testEventLaunched: testData.events[6],
      };

      assert.deepEqual(configObj.eventKeyMap, expectedEventKeyMap);

      var expectedExperimentIdMap = {
        '111127': configObj.experiments[0],
        '122227': configObj.experiments[1],
        '133337': configObj.experiments[2],
        '144447': configObj.experiments[3],
        '442': configObj.experiments[4],
        '443': configObj.experiments[5],
        '444': configObj.experiments[6],
      };

      assert.deepEqual(configObj.experimentIdMap, expectedExperimentIdMap);

      var expectedVariationKeyMap = {};
      expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[0].key] =
        testData.experiments[0].variations[0];
      expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[1].key] =
        testData.experiments[0].variations[1];
      expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[0].key] =
        testData.experiments[1].variations[0];
      expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[1].key] =
        testData.experiments[1].variations[1];
      expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[0].key] =
        testData.experiments[2].variations[0];
      expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[1].key] =
        testData.experiments[2].variations[1];
      expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[0].key] =
        configObj.experiments[3].variations[0];
      expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[1].key] =
        configObj.experiments[3].variations[1];
      expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[0].key] =
        configObj.experiments[4].variations[0];
      expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[1].key] =
        configObj.experiments[4].variations[1];
      expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[0].key] =
        configObj.experiments[5].variations[0];
      expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[1].key] =
        configObj.experiments[5].variations[1];
      expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[0].key] =
        configObj.experiments[6].variations[0];
      expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[1].key] =
        configObj.experiments[6].variations[1];

      var expectedVariationIdMap = {
        '111128': testData.experiments[0].variations[0],
        '111129': testData.experiments[0].variations[1],
        '122228': testData.experiments[1].variations[0],
        '122229': testData.experiments[1].variations[1],
        '133338': testData.experiments[2].variations[0],
        '133339': testData.experiments[2].variations[1],
        '144448': testData.experiments[3].variations[0],
        '144449': testData.experiments[3].variations[1],
        '551': configObj.experiments[4].variations[0],
        '552': configObj.experiments[4].variations[1],
        '661': configObj.experiments[5].variations[0],
        '662': configObj.experiments[5].variations[1],
        '553': configObj.experiments[6].variations[0],
        '554': configObj.experiments[6].variations[1],
      };

      assert.deepEqual(configObj.variationIdMap, expectedVariationIdMap);
    });

    it('should not mutate the datafile', function() {
      var datafile = testDatafile.getTypedAudiencesConfig();
      var datafileClone = cloneDeep(datafile);
      projectConfig.createProjectConfig(datafile);
      assert.deepEqual(datafileClone, datafile);
    });

    describe('feature management', function() {
      var configObj;
      beforeEach(function() {
        configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures());
      });

      it('creates a rolloutIdMap from rollouts in the datafile', function() {
        assert.deepEqual(configObj.rolloutIdMap, testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap);
      });

      it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function() {
        assert.deepEqual(
          configObj.variationVariableUsageMap,
          testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap
        );
      });

      it('creates a featureKeyMap from feature flags in the datafile', function() {
        assert.deepEqual(configObj.featureKeyMap, testDatafile.datafileWithFeaturesExpectedData.featureKeyMap);
      });

      it('adds variations from rollout experiments to variationIdMap', function() {
        assert.deepEqual(configObj.variationIdMap['594032'], {
          variables: [
            { value: 'true', id: '4919852825313280' },
            { value: '395', id: '5482802778734592' },
            { value: '4.99', id: '6045752732155904' },
            { value: 'Hello audience', id: '6327227708866560' },
            { value: '{ "count": 2, "message": "Hello audience" }', id: '8765345281230956' },
          ],
          featureEnabled: true,
          key: '594032',
          id: '594032',
        });
        assert.deepEqual(configObj.variationIdMap['594038'], {
          variables: [
            { value: 'false', id: '4919852825313280' },
            { value: '400', id: '5482802778734592' },
            { value: '14.99', id: '6045752732155904' },
            { value: 'Hello', id: '6327227708866560' },
            { value: '{ "count": 1, "message": "Hello" }', id: '8765345281230956' },
          ],
          featureEnabled: false,
          key: '594038',
          id: '594038',
        });
        assert.deepEqual(configObj.variationIdMap['594061'], {
          variables: [
            { value: '27.34', id: '5060590313668608' },
            { value: 'Winter is NOT coming', id: '5342065290379264' },
            { value: '10003', id: '6186490220511232' },
            { value: 'false', id: '6467965197221888' },
          ],
          featureEnabled: true,
          key: '594061',
          id: '594061',
        });
        assert.deepEqual(configObj.variationIdMap['594067'], {
          variables: [
            { value: '30.34', id: '5060590313668608' },
            { value: 'Winter is coming definitely', id: '5342065290379264' },
            { value: '500', id: '6186490220511232' },
            { value: 'true', id: '6467965197221888' },
          ],
          featureEnabled: true,
          key: '594067',
          id: '594067',
        });
      });
    });
  });

  describe('projectConfig helper methods', function() {
    var testData = cloneDeep(testDatafile.getTestProjectConfig());
    var configObj;
    var createdLogger = loggerPlugin.createLogger({ logLevel: LOG_LEVEL.INFO });

    beforeEach(function() {
      configObj = projectConfig.createProjectConfig(cloneDeep(testData));
      sinon.stub(createdLogger, 'log');
    });

    afterEach(function() {
      createdLogger.log.restore();
    });

    it('should retrieve experiment ID for valid experiment key in getExperimentId', function() {
      assert.strictEqual(
        projectConfig.getExperimentId(configObj, testData.experiments[0].key),
        testData.experiments[0].id
      );
    });

    it('should throw error for invalid experiment key in getExperimentId', function() {
      assert.throws(function() {
        projectConfig.getExperimentId(configObj, 'invalidExperimentKey');
      }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey'));
    });

    it('should retrieve layer ID for valid experiment key in getLayerId', function() {
      assert.strictEqual(projectConfig.getLayerId(configObj, '111127'), '4');
    });

    it('should throw error for invalid experiment key in getLayerId', function() {
      assert.throws(function() {
        projectConfig.getLayerId(configObj, 'invalidExperimentKey');
      }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentKey'));
    });

    it('should retrieve attribute ID for valid attribute key in getAttributeId', function() {
      assert.strictEqual(projectConfig.getAttributeId(configObj, 'browser_type'), '111094');
    });

    it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() {
      assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_user_agent'), '$opt_user_agent');
    });

    it('should return null for invalid attribute key in getAttributeId', function() {
      assert.isNull(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger));
      sinon.assert.calledWithExactly(
        createdLogger.log,
        LOG_LEVEL.DEBUG,
        'PROJECT_CONFIG: Unrecognized attribute invalidAttributeKey provided. Pruning before sending event to Optimizely.'
      );
    });

    it('should return null for invalid attribute key in getAttributeId', function() {
      // Adding attribute in key map with reserved prefix
      configObj.attributeKeyMap['$opt_some_reserved_attribute'] = {
        id: '42',
        key: '$opt_some_reserved_attribute',
      };
      assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger), '42');
      sinon.assert.calledWithExactly(
        createdLogger.log,
        LOG_LEVEL.WARN,
        'Attribute $opt_some_reserved_attribute unexpectedly has reserved prefix $opt_; using attribute ID instead of reserved attribute name.'
      );
    });

    it('should retrieve event ID for valid event key in getEventId', function() {
      assert.strictEqual(projectConfig.getEventId(configObj, 'testEvent'), '111095');
    });

    it('should return null for invalid event key in getEventId', function() {
      assert.isNull(projectConfig.getEventId(configObj, 'invalidEventKey'));
    });

    it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() {
      assert.strictEqual(
        projectConfig.getExperimentStatus(configObj, testData.experiments[0].key),
        testData.experiments[0].status
      );
    });

    it('should throw error for invalid experiment key in getExperimentStatus', function() {
      assert.throws(function() {
        projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey');
      }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey'));
    });

    it('should return true if experiment status is set to Running in isActive', function() {
      assert.isTrue(projectConfig.isActive(configObj, 'testExperiment'));
    });

    it('should return false if experiment status is not set to Running in isActive', function() {
      assert.isFalse(projectConfig.isActive(configObj, 'testExperimentNotRunning'));
    });

    it('should return true if experiment status is set to Running in isRunning', function() {
      assert.isTrue(projectConfig.isRunning(configObj, 'testExperiment'));
    });

    it('should return false if experiment status is not set to Running in isRunning', function() {
      assert.isFalse(projectConfig.isRunning(configObj, 'testExperimentLaunched'));
    });

    it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() {
      assert.deepEqual(
        projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id),
        testData.experiments[0].variations[0].key
      );
    });

    it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() {
      assert.deepEqual(
        projectConfig.getTrafficAllocation(configObj, testData.experiments[0].key),
        testData.experiments[0].trafficAllocation
      );
    });

    it('should throw error for invalid experient key in getTrafficAllocation', function() {
      assert.throws(function() {
        projectConfig.getTrafficAllocation(configObj, 'invalidExperimentKey');
      }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey'));
    });

    describe('#getVariationIdFromExperimentAndVariationKey', function() {
      it('should return the variation id for the given experiment key and variation key', function() {
        assert.strictEqual(
          projectConfig.getVariationIdFromExperimentAndVariationKey(
            configObj,
            testData.experiments[0].key,
            testData.experiments[0].variations[0].key
          ),
          testData.experiments[0].variations[0].id
        );
      });
    });

    describe('feature management', function() {
      var featureManagementLogger = loggerPlugin.createLogger({ logLevel: LOG_LEVEL.INFO });
      beforeEach(function() {
        configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures());
        sinon.stub(featureManagementLogger, 'log');
      });

      afterEach(function() {
        featureManagementLogger.log.restore();
      });

      describe('getVariableForFeature', function() {
        it('should return a variable object for a valid variable and feature key', function() {
          var featureKey = 'test_feature_for_experiment';
          var variableKey = 'num_buttons';
          var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger);
          assert.deepEqual(result, {
            type: 'integer',
            key: 'num_buttons',
            id: '4792309476491264',
            defaultValue: '10',
          });
        });

        it('should return null for an invalid variable key and a valid feature key', function() {
          var featureKey = 'test_feature_for_experiment';
          var variableKey = 'notARealVariable____';
          var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger);
          assert.strictEqual(result, null);
          sinon.assert.calledOnce(featureManagementLogger.log);
          sinon.assert.calledWithExactly(
            featureManagementLogger.log,
            LOG_LEVEL.ERROR,
            'PROJECT_CONFIG: Variable with key "notARealVariable____" associated with feature with key "test_feature_for_experiment" is not in datafile.'
          );
        });

        it('should return null for an invalid feature key', function() {
          var featureKey = 'notARealFeature_____';
          var variableKey = 'num_buttons';
          var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger);
          assert.strictEqual(result, null);
          sinon.assert.calledOnce(featureManagementLogger.log);
          sinon.assert.calledWithExactly(
            featureManagementLogger.log,
            LOG_LEVEL.ERROR,
            'PROJECT_CONFIG: Feature key notARealFeature_____ is not in datafile.'
          );
        });

        it('should return null for an invalid variable key and an invalid feature key', function() {
          var featureKey = 'notARealFeature_____';
          var variableKey = 'notARealVariable____';
          var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger);
          assert.strictEqual(result, null);
          sinon.assert.calledOnce(featureManagementLogger.log);
          sinon.assert.calledWithExactly(
            featureManagementLogger.log,
            LOG_LEVEL.ERROR,
            'PROJECT_CONFIG: Feature key notARealFeature_____ is not in datafile.'
          );
        });
      });

      describe('getVariableValueForVariation', function() {
        it('returns a value for a valid variation and variable', function() {
          var variation = configObj.variationIdMap['594096'];
          var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons;
          var result = projectConfig.getVariableValueForVariation(
            configObj,
            variable,
            variation,
            featureManagementLogger
          );
          assert.strictEqual(result, '2');

          variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated;
          result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger);
          assert.strictEqual(result, 'true');

          variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt;
          result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger);
          assert.strictEqual(result, 'Buy me NOW');

          variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width;
          result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger);
          assert.strictEqual(result, '20.25');
        });

        it('returns null for a null variation', function() {
          var variation = null;
          var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons;
          var result = projectConfig.getVariableValueForVariation(
            configObj,
            variable,
            variation,
            featureManagementLogger
          );
          assert.strictEqual(result, null);
        });

        it('returns null for a null variable', function() {
          var variation = configObj.variationIdMap['594096'];
          var variable = null;
          var result = projectConfig.getVariableValueForVariation(
            configObj,
            variable,
            variation,
            featureManagementLogger
          );
          assert.strictEqual(result, null);
        });

        it('returns null for a null variation and null variable', function() {
          var variation = null;
          var variable = null;
          var result = projectConfig.getVariableValueForVariation(
            configObj,
            variable,
            variation,
            featureManagementLogger
          );
          assert.strictEqual(result, null);
        });

        it('returns null for a variation whose id is not in the datafile', function() {
          var variation = {
            key: 'some_variation',
            id: '999999999999',
            variables: [],
          };
          var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons;
          var result = projectConfig.getVariableValueForVariation(
            configObj,
            variable,
            variation,
            featureManagementLogger
          );
          assert.strictEqual(result, null);
        });

        it('returns null if the variation does not have a value for this variable', function() {
          var variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it
          var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons;
          var result = projectConfig.getVariableValueForVariation(
            configObj,
            variable,
            variation,
            featureManagementLogger
          );
          assert.isNull(result);
        });
      });

      describe('getTypeCastValue', function() {
        it('can cast a boolean', function() {
          var result = projectConfig.getTypeCastValue('true', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger);
          assert.strictEqual(result, true);
          result = projectConfig.getTypeCastValue('false', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger);
          assert.strictEqual(result, false);
        });

        it('can cast an integer', function() {
          var result = projectConfig.getTypeCastValue('50', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger);
          assert.strictEqual(result, 50);
          var result = projectConfig.getTypeCastValue('-7', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger);
          assert.strictEqual(result, -7);
          var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger);
          assert.strictEqual(result, 0);
        });

        it('can cast a double', function() {
          var result = projectConfig.getTypeCastValue('89.99', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger);
          assert.strictEqual(result, 89.99);
          var result = projectConfig.getTypeCastValue(
            '-257.21',
            FEATURE_VARIABLE_TYPES.DOUBLE,
            featureManagementLogger
          );
          assert.strictEqual(result, -257.21);
          var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger);
          assert.strictEqual(result, 0);
          var result = projectConfig.getTypeCastValue('10', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger);
          assert.strictEqual(result, 10);
        });

        it('can return a string unmodified', function() {
          var result = projectConfig.getTypeCastValue(
            'message',
            FEATURE_VARIABLE_TYPES.STRING,
            featureManagementLogger
          );
          assert.strictEqual(result, 'message');
        });

        it('returns null and logs an error for an invalid boolean', function() {
          var result = projectConfig.getTypeCastValue(
            'notabool',
            FEATURE_VARIABLE_TYPES.BOOLEAN,
            featureManagementLogger
          );
          assert.strictEqual(result, null);
          sinon.assert.calledWithExactly(
            featureManagementLogger.log,
            LOG_LEVEL.ERROR,
            'PROJECT_CONFIG: Unable to cast value notabool to type boolean, returning null.'
          );
        });

        it('returns null and logs an error for an invalid integer', function() {
          var result = projectConfig.getTypeCastValue(
            'notanint',
            FEATURE_VARIABLE_TYPES.INTEGER,
            featureManagementLogger
          );
          assert.strictEqual(result, null);
          sinon.assert.calledWithExactly(
            featureManagementLogger.log,
            LOG_LEVEL.ERROR,
            'PROJECT_CONFIG: Unable to cast value notanint to type integer, returning null.'
          );
        });

        it('returns null and logs an error for an invalid double', function() {
          var result = projectConfig.getTypeCastValue(
            'notadouble',
            FEATURE_VARIABLE_TYPES.DOUBLE,
            featureManagementLogger
          );
          assert.strictEqual(result, null);
          sinon.assert.calledWithExactly(
            featureManagementLogger.log,
            LOG_LEVEL.ERROR,
            'PROJECT_CONFIG: Unable to cast value notadouble to type double, returning null.'
          );
        });
      });
    });

    describe('#getAudiencesById', function() {
      beforeEach(function() {
        configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig());
      });

      it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() {
        assert.deepEqual(projectConfig.getAudiencesById(configObj), testDatafile.typedAudiencesById);
      });
    });

    describe('#getExperimentAudienceConditions', function() {
      it('should retrieve audiences for valid experiment key', function() {
        configObj = projectConfig.createProjectConfig(cloneDeep(testData));
        assert.deepEqual(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].key), [
          '11154',
        ]);
      });

      it('should throw error for invalid experiment key', function() {
        configObj = projectConfig.createProjectConfig(cloneDeep(testData));
        assert.throws(function() {
          projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentKey');
        }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey'));
      });

      it('should return experiment audienceIds if experiment has no audienceConditions', function() {
        configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig());
        var result = projectConfig.getExperimentAudienceConditions(configObj, 'feat_with_var_test');
        assert.deepEqual(result, [
          '3468206642',
          '3988293898',
          '3988293899',
          '3468206646',
          '3468206647',
          '3468206644',
          '3468206643',
        ]);
      });

      it('should return experiment audienceConditions if experiment has audienceConditions', function() {
        configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig());
        // audience_combinations_experiment has both audienceConditions and audienceIds
        // audienceConditions should be preferred over audienceIds
        var result = projectConfig.getExperimentAudienceConditions(configObj, 'audience_combinations_experiment');
        assert.deepEqual(result, [
          'and',
          ['or', '3468206642', '3988293898'],
          ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'],
        ]);
      });
    });

    describe('#isFeatureExperiment', function() {
      it('returns true for a feature test', function() {
        var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures());
        var result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature'
        assert.isTrue(result);
      });

      it('returns false for an A/B test', function() {
        var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig());
        var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment'
        assert.isFalse(result);
      });

      it('returns true for a feature test in a mutex group', function() {
        var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig());
        var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1'
        assert.isTrue(result);
        result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2'
        assert.isTrue(result);
      });
    });
  });

  describe('#tryCreatingProjectConfig', function() {
    var stubJsonSchemaValidator;
    beforeEach(function() {
      stubJsonSchemaValidator = {
        validate: sinon.stub().returns(true),
      };
      sinon.stub(configValidator, 'validateDatafile').returns(true);
      sinon.spy(logger, 'error');
    });

    afterEach(function() {
      configValidator.validateDatafile.restore();
      logger.error.restore();
    });

    it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', function() {
      var configDatafile = {
        foo: 'bar',
        experiments: [
          {key: 'a'},
          {key: 'b'}
        ]
      }
      configValidator.validateDatafile.returns(configDatafile);
      var configObj = {
        foo: 'bar',
        experimentKeyMap: {
          "a": { key: "a", variationKeyMap: {} },
          "b": { key: "b", variationKeyMap: {} }
        },
      };

      stubJsonSchemaValidator.validate.returns(true);

      var result = projectConfig.tryCreatingProjectConfig({
        datafile: configDatafile,
        jsonSchemaValidator: stubJsonSchemaValidator,
        logger: logger,
      });

      assert.deepInclude(result.configObj, configObj)
    });

    it('returns an error when validateDatafile throws', function() {
      configValidator.validateDatafile.throws();
      stubJsonSchemaValidator.validate.returns(true);
      var { error } = projectConfig.tryCreatingProjectConfig({
        datafile: { foo: 'bar' },
        jsonSchemaValidator: stubJsonSchemaValidator,
        logger: logger,
      });
      assert.isNotNull(error);
    });

    it('returns an error when jsonSchemaValidator.validate throws', function() {
      configValidator.validateDatafile.returns(true);
      stubJsonSchemaValidator.validate.throws();
      var { error } = projectConfig.tryCreatingProjectConfig({
        datafile: { foo: 'bar' },
        jsonSchemaValidator: stubJsonSchemaValidator,
        logger: logger,
      });
    assert.isNotNull(error);
    });

    it('skips json validation when jsonSchemaValidator is not provided', function() {

      var configDatafile = {
        foo: 'bar',
        experiments: [
          {key: 'a'},
          {key: 'b'}
        ]
      }

      configValidator.validateDatafile.returns(configDatafile);

      var configObj = {
        foo: 'bar',
        experimentKeyMap: {
          a: { key: 'a', variationKeyMap: {} },
          b: { key: 'b', variationKeyMap: {} },
        },
      };

      var result = projectConfig.tryCreatingProjectConfig({
        datafile: configDatafile,
        logger: logger,
      });

      assert.deepInclude(result.configObj, configObj);
      sinon.assert.notCalled(logger.error);
    });
  });
});
