/*!
 * Module dependencies.
 */
var path = require('path');
var util = require('util');
var events = require('events');
var HttpRequest = require('./http/request.js');
var CommandQueue = require('./core/queue.js');
var Assertion = require('./core/assertion.js');
var Logger = require('./util/logger.js');
var Api = require('./core/api.js');
var Utils = require('./util/utils.js');

function Nightwatch(options) {
  events.EventEmitter.call(this);

  this.locateStrategy = 'css selector';

  this.api = {
    capabilities : {},
    globals : options && options.persist_globals && options.globals || {},
    sessionId : null
  };

  this.setMaxListeners(0);
  this.sessionId = null;
  this.context = null;
  this.terminated = false;

  this.setOptions(options)
      .setCapabilities()
      .loadKeyCodes();

  this.errors = [];
  this.results = {
    passed:0,
    failed:0,
    errors:0,
    skipped:0,
    tests:[]
  };

  Assertion.init(this);
  Api.init(this).load();

  this.queue = CommandQueue;
  this.queue.empty();
  this.queue.reset();
}

Nightwatch.DEFAULT_CAPABILITIES = {
  browserName: 'firefox',
  javascriptEnabled: true,
  acceptSslCerts: true,
  platform: 'ANY'
};

util.inherits(Nightwatch, events.EventEmitter);

Nightwatch.prototype.assertion = Assertion.assert;

Nightwatch.prototype.setOptions = function(options) {
  this.options = {};
  this.api.options = {};
  if (options && typeof options == 'object') {
    for (var propName in options) {
      this.options[propName] = options[propName];
    }
  }

  this.api.launchUrl = this.options.launchUrl || this.options.launch_url || null;
  // backwords compatibility
  this.api.launch_url = this.api.launchUrl;

  if (this.options.globals && typeof this.options.globals == 'object' && !this.options.persist_globals) {
    for (var globalKey in this.options.globals) {
      this.api.globals[globalKey] = this.options.globals[globalKey];
    }
  }

  var screenshots = this.options.screenshots;
  var screenshotsEnabled = screenshots && screenshots.enabled || false;

  this.api.options.screenshots = screenshotsEnabled;
  if (screenshotsEnabled) {
    if (typeof screenshots.path == 'undefined') {
      throw new Error('Please specify the screenshots.path in nightwatch.json.');
    }
    this.options.screenshots.on_error = this.options.screenshots.on_error ||
      (typeof this.options.screenshots.on_error == 'undefined');
    this.api.screenshotsPath = this.api.options.screenshotsPath = screenshots.path;
  } else {
    this.options.screenshots = {
      enabled : false,
      path : ''
    };
  }

  if (this.options.use_xpath) {
    this.locateStrategy = 'xpath';
  }

  if (this.options.silent) {
    Logger.disable();
  } else {
    Logger.enable();
  }

  this.options.start_session = this.options.start_session || (typeof this.options.start_session == 'undefined');

  this.api.options.skip_testcases_on_fail = this.options.skip_testcases_on_fail ||
    (typeof this.options.skip_testcases_on_fail == 'undefined' && this.options.start_session); // off by default for unit tests

  this.api.options.log_screenshot_data = this.options.log_screenshot_data ||
    (typeof this.options.log_screenshot_data == 'undefined');
  var seleniumPort = this.options.seleniumPort || this.options.selenium_port;
  var seleniumHost = this.options.seleniumHost || this.options.selenium_host;
  var useSSL       = this.options.useSsl || this.options.use_ssl;
  var proxy        = this.options.proxy;

  if (seleniumPort) {
    HttpRequest.setSeleniumPort(seleniumPort);
  }
  if (seleniumHost) {
    HttpRequest.setSeleniumHost(seleniumHost);
  }
  if (useSSL) {
    HttpRequest.useSSL(true);
  }
  if (proxy) {
    HttpRequest.setProxy(proxy);
  }

  if (typeof this.options.default_path_prefix == 'string') {
    HttpRequest.setDefaultPathPrefix(this.options.default_path_prefix);
  }

  var username = this.options.username;
  var key = this.options.accesKey || this.options.access_key || this.options.password;

  if (username && key) {
    this.api.options.username = username;
    this.api.options.accessKey = key;

    HttpRequest.setCredentials({
      username : username,
      key : key
    });
  }

  this.endSessionOnFail(typeof this.options.end_session_on_fail == 'undefined' || this.options.end_session_on_fail);

  return this;
};

Nightwatch.prototype.endSessionOnFail = function(value) {
  if (typeof value == 'undefined') {
    return this.options.end_session_on_fail;
  }

  this.options.end_session_on_fail = value;
  return this;
};

Nightwatch.prototype.setCapabilities = function() {
  this.desiredCapabilities = {};
  for (var capability in Nightwatch.DEFAULT_CAPABILITIES) {
    this.desiredCapabilities[capability] = Nightwatch.DEFAULT_CAPABILITIES[capability];
  }

  if (this.options.desiredCapabilities) {
    for (var prop in this.options.desiredCapabilities) {
      if (this.options.desiredCapabilities.hasOwnProperty(prop)) {
        this.desiredCapabilities[prop] = this.options.desiredCapabilities[prop];
      }
    }
  }
  this.api.options.desiredCapabilities = this.desiredCapabilities;
  return this;
};

Nightwatch.prototype.loadKeyCodes = function() {
  this.api.Keys = require('./util/keys.json');
  return this;
};

Nightwatch.prototype.start = function() {
  if (!this.sessionId && this.options.start_session) {
    this
      .once('selenium:session_create', this.start)
      .startSession();
    return this;
  }

  var self = this;

  this.queue.reset();
  this.queue.run(function(error) {
    if (error) {
      var stackTrace = '';
      if (error.stack) {
        stackTrace = error.stack.split('\n').slice(1).join('\n');
      }

      self.results.errors++;
      self.errors.push(error.name + ': ' + error.message + '\n' + stackTrace);
      if (self.options.output) {
        Utils.showStackTraceWithHeadline(error.name + ': ' + error.message, stackTrace, true);
      }

      if (self.options.start_session) {
        self.terminate();
      }
      return;
    }

    self.finished();
  });

  return this;
};

Nightwatch.prototype.terminate = function(deferred) {
  // in case this was a synchronous command (e.g. assert.ok()) we need to wait for other possible
  // commands which might have been added afterwards while client is terminated
  if (deferred) {
    this.queue.instance().once('queue:started', this.terminateSession.bind(this));
  } else {
    this.terminateSession();
  }

  this.terminated = true;
  return this;
};

Nightwatch.prototype.resetTerminated = function() {
  this.terminated = false;
  return this;
};

Nightwatch.prototype.terminateSession = function() {
  this.queue.reset();
  this.queue.empty();

  if (this.options.end_session_on_fail && this.options.start_session) {
    this.api.end(function() {
      this.finished();
    }.bind(this));

    // FIXME: sometimes the queue is incorrectly restarted when another .end() is
    // scheduled from globalBeforeEach and results into a session command being sent with
    // null as the sessionId
    this.queue.run();
  } else {
    this.finished();
  }
  return this;
};

Nightwatch.prototype.complete = function() {
  return this.emit('complete');
};

Nightwatch.prototype.finished = function() {
  Logger.info('FINISHED');

  this.emit('nightwatch:finished', this.results, this.errors);

  return this;
};

Nightwatch.prototype.getFailureMessage = function() {
  var errors = '';

  var failure_msg = [];
  if (this.results.failed > 0) {
    failure_msg.push(Logger.colors.red(this.results.failed) +
      ' assertions failed');
  }
  if (this.results.errors > 0) {
    failure_msg.push(Logger.colors.red(this.results.errors) + ' errors');
  }
  if (this.results.passed > 0) {
    failure_msg.push(Logger.colors.green(this.results.passed) + ' passed');
  }

  if (this.results.skipped > 0) {
    failure_msg.push(Logger.colors.blue(this.results.skipped) + ' skipped');
  }

  return failure_msg.join(', ').replace(/,([^,]*)$/g, function($0, $1) {
    return  ' and' + $1;
  });
};

Nightwatch.prototype.printResult = function(elapsedTime) {
  if (this.options.output && this.options.start_session) {
    var ok = false;
    if (this.results.failed === 0 && this.results.errors === 0) {
      ok = true;
    }

    if (ok && this.results.passed > 0) {
      console.log('\n' + Logger.colors.green('OK.'),
        Logger.colors.green(this.results.passed) + ' assertions passed. (' + Utils.formatElapsedTime(elapsedTime, true) + ')');
    } else if (ok && this.results.passed === 0) {
      if (this.options.start_session) {
        console.log(Logger.colors.green('No assertions ran.'));
      }
    } else {
      var failure_msg = this.getFailureMessage();
      console.log('\n' +  Logger.colors.red('FAILED: '), failure_msg, '(' + Utils.formatElapsedTime(elapsedTime, true) + ')');
    }
  }
};

Nightwatch.prototype.clearResult = function() {
  this.errors.length = 0;
  this.results.passed = 0;
  this.results.failed = 0;
  this.results.errors = 0;
  this.results.skipped = 0;
  this.results.tests.length = 0;
};

Nightwatch.prototype.handleException = function(err) {
  var stack = err.stack.split('\n');
  var failMessage = stack.shift();
  var firstLine = ' ' + String.fromCharCode(10006) + ' ' + failMessage;
  if (typeof err.actual != 'undefined' && typeof err.expected != 'undefined') {
    firstLine += '\033[0;90m - expected ' + Logger.colors.green('"' + err.expected + '"') + ' \033[0;90mbut got: ' + Logger.colors.red('"' + err.actual + '"');
  }

  if (this.options.output) {
    Utils.showStackTraceWithHeadline(firstLine, stack);
  }

  if (err.name == 'AssertionError') {
    this.results.failed++;
    stack.unshift(failMessage + ' - expected "' + err.expected + '" but got: "' + err.actual + '"');
    this.results.stackTrace = stack.join('\n');
  } else {
    this.addError('\n  ' + err.stack, firstLine);
    this.terminate();
  }
};

Nightwatch.prototype.runProtocolAction = function(requestOptions, callback) {
  var self = this;

  var request = new HttpRequest(requestOptions)
    .on('result', function(result) {
      if (typeof callback != 'function') {
        var error = new Error('Callback parameter is not a function - ' + typeof(callback) + ' passed: "' + callback + '"');
        self.errors.push(error.stack);
        self.results.errors++;
      } else {
        callback.call(self.api, result);
      }

      if (result.lastScreenshotFile && self.results.tests.length > 0) {
        var lastTest = self.results.tests[self.results.tests.length-1];
        lastTest.screenshots = lastTest.screenshots || [];
        lastTest.screenshots.push(result.lastScreenshotFile);
        delete result.lastScreenshotFile;
      }

      request.emit('complete');
    })
    .on('success', function(result, response) {
      if (result.status && result.status !== 0) {
        result = self.handleTestError(result);
      }
      request.emit('result', result, response);
    })
    .on('error', function(result, response, screenshotContent) {
      result = self.handleTestError(result);

      if (screenshotContent && self.options.screenshots.on_error) {
        var fileNamePath = Utils.getScreenshotFileName(self.api.currentTest, true, self.options.screenshots.path);
        self.saveScreenshotToFile(fileNamePath, screenshotContent);
        result.lastScreenshotFile = fileNamePath;
      }

      request.emit('result', result, response);
    });

  return request;
};

Nightwatch.prototype.addError = function(message, logMessage) {
  var currentTest;
  if (this.api.currentTest) {
    currentTest = '[' + Utils.getTestSuiteName(this.api.currentTest.module) + ' / ' + this.api.currentTest.name + ']';
  } else {
    currentTest = 'tests';
  }

  this.errors.push('  Error while running '+ currentTest  + ':\n' + message);
  this.results.errors++;
  if (this.options.output) {
    Logger.warn('    ' + (logMessage || message));
  }

};

Nightwatch.prototype.saveScreenshotToFile = function(fileName, content, cb) {
  var mkpath = require('mkpath');
  var fs = require('fs');
  var self = this;
  cb = cb || function() {};

  var dir = path.resolve(fileName, '..');
  var fail = function(err) {
    if (self.options.output) {
      console.log(Logger.colors.yellow('Couldn\'t save screenshot to '), fileName);
    }

    Logger.warn(err);
    cb(err);
  };

  mkpath(dir, function(err) {
    if (err) {
      fail(err);
    } else {
      fs.writeFile(fileName, content, 'base64', function(err) {
        if (err) {
          fail(err);
        } else {
          cb(null, fileName);
        }
      });
    }
  });
};

Nightwatch.prototype.handleTestError = function(result) {
  var errorMessage = '';
  if (result && result.status) {
    var errorCodes = require('./api/errors.json');
    errorMessage = errorCodes[result.status] && errorCodes[result.status].message || '';
  }

  return {
    status: -1,
    value : result && result.value || null,
    errorStatus: result && result.status || '',
    error : errorMessage
  };
};

Nightwatch.prototype.startSession = function () {
  if (this.terminated) {
    return this;
  }
  var self = this;
  var options = {
    path : '/session',
    data : {
      desiredCapabilities : this.desiredCapabilities
    }
  };

  var request = new HttpRequest(options);
  request.on('success', function(data, response, isRedirect) {
    if (data && data.sessionId) {
      self.sessionId = self.api.sessionId = data.sessionId;
      if (data.value) {
        self.api.capabilities = data.value;
      }
      Logger.info('Got sessionId from selenium', self.sessionId);
      self.emit('selenium:session_create', self.sessionId, request, response);
    } else if (isRedirect) {
      self.followRedirect(request, response);
    } else {
      request.emit('error', data, null);
    }
  })
  .on('error', function(data, err) {
    if (self.options.output) {
      console.error('\n' + Logger.colors.light_red('Error retrieving a new session from the selenium server'));
    }

    if (typeof data == 'object' && Object.keys(data).length === 0) {
      data = '';
    }

    if (!data && err) {
      data = err;
    }

    self.emit('error', data);
  })
  .send();

  return this;
};

Nightwatch.prototype.followRedirect = function (request, response) {
  if (!response.headers || !response.headers.location) {
    this.emit('error', null, null);
    return this;
  }
  var url = require('url');
  var urlParts = url.parse(response.headers.location);
  request.setOptions({
    path   : urlParts.pathname,
    host   : urlParts.hostname,
    port   : urlParts.port,
    method : 'GET'
  }).send();

  return this;
};

exports = module.exports = {};

exports.client = function(options) {
  return new Nightwatch(options);
};

exports.cli = function(runTests) {
  var cli = require('./runner/cli/cli.js');
  cli.setup();

  var argv = cli.init();
  if (argv.help) {
    cli.showHelp();
  } else if (argv.version) {
    var packageConfig = require(__dirname + '/../package.json');
    console.log(packageConfig.name + ' v' + packageConfig.version);
  } else {
    if (typeof runTests != 'function') {
      throw new Error('Supplied argument needs to be a function!');
    }
    runTests(argv);
  }
};

exports.runner = function(argv, done, settings) {
  var runner = exports.CliRunner(argv);
  return runner.setup(settings, done).runTests(done);
};

exports.initGrunt = function(grunt) {
  grunt.registerMultiTask('nightwatch', 'run nightwatch.', function() {
    var done = this.async();
    var options = this.options();
    var settings = this.data && this.data.settings;
    var argv = this.data && this.data.argv;

    exports.cli(function(a) {
      Object.keys(argv).forEach(function(key) {
        if (key === 'env' && a['parallel-mode'] === true) {
          return;
        }
        a[key] = argv[key];
      });

      if (a.test) {
        a.test = path.resolve(a.test);
      }

      if (options.cwd) {
        process.chdir(options.cwd);
      }

      exports.runner(a, done, settings);
    });
  });
};

exports.CliRunner = function(argv) {
  var CliRunner = require('./runner/cli/clirunner.js');
  return new CliRunner(argv);
};

exports.initClient = function(opts) {
  var Manager = require('./runner/clientmanager.js');
  var instance = new Manager();
  return instance.init(opts);
};
