/// <summary>
        /// Initializes a new instance of the <see cref="BatchCaptureJobInfo"/> class.
        /// </summary>
        /// <param name="url">
        /// The URL to capture.
        /// </param>
        /// <param name="filenameTemplate">
        /// The filename template that will be used to capture the screenshot.
        /// </param>
        /// <param name="jobInfo">
        /// The job information.
        /// </param>
        /// <param name="browsers">
        /// The browsers.
        /// </param>
        public BatchCaptureJobInfo(string url, string filenameTemplate, Job.JobInfo jobInfo, params Browser[] browsers)
        {
            Contract.Requires(!string.IsNullOrEmpty(url));
            Contract.Requires(!string.IsNullOrEmpty(filenameTemplate));
            Contract.Requires(!illegalFileNameCharactersRegex.IsMatch(filenameTemplate), "The filename contains illegal characters.");
            Contract.Requires(jobInfo != null);
            Contract.Requires(browsers != null);
            Contract.Requires(browsers.Length > 1);

            this.Url = url;
            this.Filename = filenameTemplate;
            this.JobInfo = jobInfo;
            this.Browsers = browsers;
        }
        /// <summary>
        /// Starts a BrowserStack screenshot job asynchronously.
        /// </summary>
        /// <param name="url">The url for which screenshots are required.</param>
        /// <param name="jobInfo">The job information that will be used to start the job.</param>
        /// <param name="usingTunnel">set to <c>true</c> if the BrowserStack jobs need to run under a tunnel. The tunnel must have been initiated externally.</param>
        /// <param name="browsers">The browsers that will be used to start the job.</param>
        /// <returns>
        /// The <see cref="Task" />.
        /// </returns>
        /// <exception cref="ApplicationException">Thrown when the call to the BrowserStack API results in an http code other than 200 (OK).</exception>
        public async Task<Job> StartJobAsync(string url, Job.JobInfo jobInfo, bool usingTunnel = false, params Browser[] browsers)
        {
            var jobRequest = new ScreenshotJobRequest()
            {
                url = url, 
                callback_url = jobInfo.CallbackUrl, 
                orientation = jobInfo.Orientation.HasValue ? jobInfo.Orientation.ToString().ToLower() : null, 
                quality = jobInfo.Quality.HasValue ? jobInfo.Quality.ToString().ToLower() : null, 
                wait_time = jobInfo.WaitTime, 
                mac_res = jobInfo.OsxResolution.HasValue ? jobInfo.OsxResolution.ToString().ToLower().Replace("r_", string.Empty) : null, 
                win_res = jobInfo.WinResolution.HasValue ? jobInfo.WinResolution.ToString().ToLower().Replace("r_", string.Empty) : null, 
                tunnel = usingTunnel, 
                browsers = browsers.Select(x => new BrowserInfo() { browser = x.BrowserName, browser_version = x.BrowserVersion, device = x.Device, os = x.OS, os_version = x.OSVersion, }).ToArray()
            };

            using (var httpClient = this.httpClientFactory.Invoke())
            {
                var request = new HttpRequestMessage(HttpMethod.Post, screenshotsRestAPIBaseUrl)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(jobRequest), Encoding.Default, "application/json")
                };
                if (this.AuthenticateForStartJob)
                {
                    request.Headers.Authorization = this.GetAuthenticationHeader();
                }

                var response = await httpClient.SendAsync(request);
                var responseString = await response.Content.ReadAsStringAsync();

                if (!response.IsSuccessStatusCode)
                {
                    throw new ApplicationException(
                        string.Format("Error while starting the job.\nResponse status is {0} ({1}).\nResponse is: {2}", response.ReasonPhrase, response.StatusCode, responseString));
                }

                var screenshotJob = await JsonConvert.DeserializeObjectAsync<ScreenshotJob>(responseString);

                return MapToJob(screenshotJob);
            }
        }
 /// <summary>
 /// Initializes a new instance of the <see cref="JobEventArgs"/> class.
 /// </summary>
 /// <param name="job">
 /// The job.
 /// </param>
 public JobEventArgs(Job job)
 {
     this.Job = job;
     this.State = job.State;
 }
 /// <summary>
 /// Initializes a new instance of the <see cref="JobFailureEventArgs" /> class.
 /// </summary>
 /// <param name="jobUrl">The job URL.</param>
 /// <param name="jobInfo">The job information.</param>
 /// <param name="browsers">The browsers.</param>
 /// <param name="exception">The exception that caused the failure.</param>
 public JobFailureEventArgs(string jobUrl, Job.JobInfo jobInfo, IEnumerable<Browser> browsers, Exception exception)
 {
     this.Exception = exception;
     this.Browsers = browsers;
     this.JobInfo = jobInfo;
     this.JobUrl = jobUrl;
 }
        /// <summary>
        /// The save screenshot async.
        /// </summary>
        /// <param name="screenshot">The screenshot.</param>
        /// <param name="jobId">The job identifier.</param>
        /// <param name="jobInfo">The job info.</param>
        /// <param name="rootPath">The root path.</param>
        /// <returns>
        /// The <see cref="Task" />.
        /// </returns>
        private async Task SaveScreenshotAsync(Screenshot screenshot, string jobId, Job.JobInfo jobInfo, string rootPath)
        {
            // Create the folder for the screenshot
            var directory = this.CreateScreenshotDirectory(jobInfo, screenshot.Browser, rootPath);
            var filename = this.jobsToJobsToRun[jobId.ToLower()].Filename;
            
            try
            {
                if (screenshot.State == Screenshot.States.Done)
                {
                    var tasksToWaitFor = new List<Task>();
                    tasksToWaitFor.Add(this.screenshotsApi.SaveScreenshotToFileAsync(screenshot, directory.FullName, filename, false));

                    if (this.captureThumbnails)
                    {
                        tasksToWaitFor.Add(this.screenshotsApi.SaveThumbnailToFileAsync(screenshot, directory.FullName, filename + "_thumbnail", false));
                    }

                    await Task.WhenAll(tasksToWaitFor.ToArray());
                }
            }
            catch (Exception ex)
            {
                this.OnScreenshotFailed(new ScreenshotFailedEventArgs(screenshot, ex, filename));
            }

            this.ScreenhotsCompleted.Add(screenshot);
            this.OnScreenshotCompleted(new ScreenshotEventArgs(screenshot));
        }
        /// <summary>
        /// The create screenshot directory.
        /// </summary>
        /// <param name="jobInfo">The job info.</param>
        /// <param name="browser">The browser.</param>
        /// <param name="rootPath">The root path.</param>
        /// <returns>
        /// The <see cref="DirectoryInfo" />.
        /// </returns>
        private DirectoryInfo CreateScreenshotDirectory(Job.JobInfo jobInfo, Browser browser, string rootPath)
        {
            const string deviceFolderStructureTemplate = @"{0}\{1}\{2}\{3}";
            const string browserFolderStructureTemplate = @"{0}\{1}\{2}\{3}\{4}";

            string screenshotFolderName;

            if (string.IsNullOrEmpty(browser.Device))
            {
                screenshotFolderName = string.Format(
                    browserFolderStructureTemplate, 
                    browser.OS, 
                    browser.OSVersion, 
                    browser.BrowserName, 
                    browser.BrowserVersion, 
                    (browser.OS.ToLower().Contains("os x") ? jobInfo.OsxResolution.ToString() : jobInfo.WinResolution.ToString()).Replace("R_", string.Empty));
            }
            else
            {
                screenshotFolderName = string.Format(deviceFolderStructureTemplate, browser.OS, browser.OSVersion, browser.Device, jobInfo.Orientation);
            }

            return Directory.CreateDirectory(Path.Combine(rootPath, screenshotFolderName));
        }