import os
from datetime import datetime
from robot import rebot
from accelarobotkeywords.framework.utils.jenkins import Jenkins
from accelarobotkeywords.runner import testrailintegration as testrail
from atlassian import Confluence
import shutil
import docker
import json
import random
import string
import requests
import re
import time

test_dirs = [
    'aa/tests',
    'aca/tests',
    'agis/tests',
    'cam/ui/tests',
    'services/src/tests'
]

class ParallelRunner:
    IMAGE_NAME = 'robotfwk'
    ENTRY_SCRIPT = './execute-suite.sh'
    ROBOT_XML = 'SUITE_output.xml'
    JUNIT_XML = 'SUITE_junit.xml'
    ANNOTATIONS_JSON = "SUITE_annotations.json"
    HTML_REPORT = "allsuites_report.html"
    HTML_LOG = "allsuites_log.html"
    XML_REPORT = "allsuites_junit.xml"

    def __init__(self, execution_file, suite_base_path, output_base_path, max_suites, robot_tags, report_title,
                 dev_mode, test_suite_name, test_case_list, agency, civic_platform_version, test_rail_result_enabled,
                 reruntests, testrail_project_id, testrail_suite_id, confluence_attachment_name, browser_location,
                 desired_capabilities, zap_enabled, proxy_host, proxy_port, proxy_path, network_logger_enabled):
        self.execution_file_path = execution_file
        self.suite_base_path = suite_base_path
        self.output_base_path = output_base_path
        self.max_suites = max_suites
        self.robot_tags = robot_tags
        self.report_title = report_title
        self.dev_mode = dev_mode
        self.test_suite_name = test_suite_name
        self.test_case_list = test_case_list
        self.agency = agency
        self.civic_platform_version = civic_platform_version
        self.test_rail_result_enabled = test_rail_result_enabled
        self.jenkins_build_number = ''
        self.execution_file_json = None
        self.output_path = None
        self.suite_batches = None
        self.execution_name = None
        self.environment_file = None
        self.reruntests = reruntests
        self.testrail_project_id = testrail_project_id
        self.testrail_suite_id = testrail_suite_id
        self.confluence_attachment_name = confluence_attachment_name
        self.browser_location = browser_location
        self.desired_capabilities = desired_capabilities
        self.network_logger_enabled = network_logger_enabled
        self.zap_enabled = zap_enabled
        self.proxy_host = proxy_host
        self.proxy_port = proxy_port
        self.proxy_path = proxy_path

    def execute_suites(self):
        if self.dev_mode > 0:
            if self.test_suite_name.upper() == 'FULL_CICD_SUITE.ROBOT':
                try:
                    print('Executing test case(s) from : %s' % self.test_suite_name)
                    self.__setup_output_directory()
                    cicd_suite = self.__build_dynamic_suite_file()
                    self.execution_file_json = cicd_suite
                    self.__divide_into_batches()
                    batch_count = 1
                    if self.confluence_attachment_name:
                        tr_report = testrail.TestrailIntegration()
                        tr_report.add_testrail_run(self.output_path, self.civic_platform_version, self.jenkins_build_number,
                                       self.test_rail_result_enabled, self.testrail_project_id, self.testrail_suite_id)
                    for batch in self.suite_batches:
                        try:
                            print('Execute batch %s set of containers' % str(batch_count))
                            self.__execute_batch_of_containers(batch)
                            print('Batch %s completed' % str(batch_count))
                            batch_count += 1
                        except Exception:
                            pass
                    self.__merge_container_reports()
                    self.__copy_files_to_jenkins_workspace()
                    print('Execution completed')
                except Exception as e:
                    print('Error executing full CICD suite : %s' % str(e))
                    raise
            else:
                try:
                    print('Executing test case(s) from : %s' % self.test_suite_name)
                    self.__setup_output_directory()
                    single_suite = [
                        {"suitefile": self.test_suite_name, "agency": self.agency, "testcases": self.test_case_list}]
                    self.execution_file_json = {"suites": [
                        {"suitefile": self.test_suite_name, "agency": self.agency, "testcases": self.test_case_list}]}
                    tr_report = testrail.TestrailIntegration()
                    tr_report.add_testrail_run(self.output_path, self.civic_platform_version, self.jenkins_build_number,
                                       self.test_rail_result_enabled, self.testrail_project_id, self.testrail_suite_id)
                    try:
                        print('Execute Suite Test(s) in a container')
                        self.__execute_batch_of_containers(single_suite)
                        print('Single Suite Test(s) is completed')
                    except Exception:
                        pass
                    if  self.reruntests > -1:
                        self.__merge_container_reports()
                        self.__copy_files_to_jenkins_workspace()
                    print('Execution completed')
                except Exception as e:
                    print('Error executing single suite : %s' % str(e))
                    raise
        else:
            print('Executing parallel suites from file: %s' % self.execution_file_path)
            try:
                self.__load_execution_file()
                self.__setup_output_directory()
                self.__divide_into_batches()
                batch_count = 1
                tr_report = testrail.TestrailIntegration()
                tr_report.add_testrail_run(self.output_path, self.civic_platform_version, self.jenkins_build_number,
                                       self.test_rail_result_enabled, self.testrail_project_id, self.testrail_suite_id)
                for batch in self.suite_batches:
                    try:
                        print('Execute batch %s set of containers' % str(batch_count))
                        self.__execute_batch_of_containers(batch)
                        print('Batch %s completed' % str(batch_count))
                        batch_count += 1
                    except Exception:
                        pass
                self.__merge_container_reports()
                self.__copy_files_to_jenkins_workspace()
                print('Execution completed')
            except Exception as e:
                print('Error executing parallel suites : %s' % str(e))
                raise

    def __merge_container_reports(self):
        """Merges the robot framework output files from each container"""
        print('Copying container output xml files to top level')
        files_to_merge = []
        for suite in self.execution_file_json['suites']:
            try:
                if 'suitefile' in suite:
                    name = suite['suitefile'].replace('.robot', '')
                else:
                    name = suite['suitedirectory']
                print('Copying xml file for suite: %s' % name)
                output_xml_path = os.path.join(self.output_path, name, ParallelRunner.ROBOT_XML.replace('SUITE', name))
                destination_path = os.path.join(self.output_path, ParallelRunner.ROBOT_XML.replace('SUITE', name))
                shutil.copyfile(src=output_xml_path, dst=destination_path)
                retries = 6
                while retries > 0:
                    if os.path.exists(output_xml_path) and os.path.getsize(destination_path) > 0:
                        files_to_merge.append(destination_path)
                        break
                    print(f"[WAIT] XML not ready for {name}, retrying...")
                    time.sleep(0.5)
                    retries -= 1
                if retries == 0:
                    print(f"[SKIP] XML for suite {name} still empty after retries.")
            except Exception as e:
                print(f"Error copying XML for suite {suite}: {e}")
        print('Merging container output xml into html report')
        try:
            log_path = os.path.join(self.output_path, 'allsuites_log.html')
            report_path = os.path.join(self.output_path, 'allsuites_report.html')
            junit_path = os.path.join(self.output_path, 'allsuites_junit.xml')
            rebot(*files_to_merge, tagstatinclude='tag TestType:*', name=self.report_title, log=log_path,
                  report=report_path, output=junit_path)
        except Exception as e:
            print('Error merging container xml output: %s' % str(e))
            raise

    def __divide_into_batches(self):
        """Divides the list of suites into batches as per max suites"""
        print('Creating batches for parallel execution')
        num_suites = len(self.execution_file_json['suites'])
        full_batches = num_suites // self.max_suites
        print('- Full batches=%s' % full_batches)
        if num_suites % self.max_suites > 0:
            has_partial = True
        else:
            has_partial = False
        print('- Partial batch at end: %s' % has_partial)
        if has_partial:
            total_batches = full_batches + 1
        else:
            total_batches = full_batches
        print('- %s suites will be divided into %s container batches using max suites %s' % (
            num_suites, total_batches, self.max_suites))
        self.suite_batches = []
        # split full batches
        for batch_counter in range(0, full_batches):
            start_index = batch_counter * self.max_suites
            batch = []
            for counter in range(start_index, start_index + self.max_suites):
                batch.append(self.execution_file_json['suites'][counter])
            self.suite_batches.append(batch)
        print('- full batches created', self.suite_batches)
        # add partial batch
        if has_partial:
            start_index = full_batches * self.max_suites
            batch = []
            for counter in range(start_index, num_suites):
                batch.append(self.execution_file_json['suites'][counter])
            self.suite_batches.append(batch)
            print('- partial batch created', self.suite_batches)

    def __load_execution_file(self):
        """Loads the execution file and checks all suites exist, also basic validation of the json file"""
        with open(self.execution_file_path, 'r') as json_file:
            self.execution_file_json = json.load(json_file)
        # check each suite directory or file exists
        for suite in self.execution_file_json['suites']:
            if 'suitefile' in suite:
                suite_file_path = os.path.join(self.suite_base_path, suite['suitefile'])
                if not os.path.isfile(suite_file_path):
                    print('Suite file not found: %s' % suite_file_path)
                    raise Exception('Suite file not found, aborting: %s' % suite_file_path)
            elif 'suitedirectory' in suite:
                suite_dir_path = os.path.join(self.suite_base_path, suite['suitedirectory'])
                if not os.path.isdir(suite_dir_path):
                    print('Suite directory not found, aborting: %s' % suite_dir_path)
                    raise Exception('Suite directory not found, aborting: %s' % suite_dir_path)
            else:
                print('No suitefile or suitedirectory specified for suite')
                raise Exception('No suitefile or suitedirectory specified for suite, aborting')
        # check suite name specified
        if 'name' in self.execution_file_json:
            print('Execution list name: %s' % self.execution_file_json['name'])
            self.execution_name = self.execution_file_json['name'].replace(' ', '_')
        else:
            print('No execution name was specified in json file')
            raise Exception('No execution name specified in json file, aborting')
        # get environment file name
        if 'environmentfile' in self.execution_file_json:
            print('Execution environment file: %s' % self.execution_file_json['environmentfile'])
            self.environment_file = self.execution_file_json['environmentfile']
        else:
            self.environment_file = 'NotSet'
        print('Suite execution file was loaded')

    def __setup_output_directory(self):
        """Sets up the output directory in the output base path"""
        print('Setting up output directory')
        if Jenkins.is_running_from_jenkins():
            # executing from jenkins
            print('- Executing from jenkins')
            job_name = os.getenv('JOB_NAME')
            build_number = os.getenv('BUILD_NUMBER')
            if len(self.output_base_path) <= 15:
                self.output_path = os.path.join(self.output_base_path, job_name, build_number)
            else:   self.output_path = self.output_base_path
            self.jenkins_build_number = build_number
        else:
            print('- Not executing from Jenkins')
            time_stamp = datetime.now().strftime("%d-%m-%Y-%H-%M-%S")
            self.output_path = os.path.join(self.output_base_path, '%s_%s' % (self.execution_name, time_stamp))
        print('- Creating output directory: %s' % self.output_path)
        if len(self.output_base_path) <= 15:
            try:
                os.makedirs(self.output_path)
            except:
                shutil.rmtree(self.output_path)
                os.mkdir(self.output_path)
        print('- Output directory created')

    def __execute_batch_of_containers(self, batch_suites):
        """Executes and waits for completion of specified list of suite names"""
        print('Executing batch of containers', batch_suites)
        try:
            containers = []
            docker_client = docker.from_env()

            # get the extra_hosts
            extra_hosts_entries = self.__read_extra_hosts()

            print('Robot Tags: %s' % self.robot_tags)
            print('Custom Report Title: %s' % self.report_title)
            for suite in batch_suites:
                self.__handle_suspend_request(self)
                # override the application base path if it is in the suite file
                if 'basepath' in suite:
                    base_path = suite['basepath']
                    print('- Overriding the default basepath for this test suite: %s' % base_path)
                else:
                    suite_base_path = self.suite_base_path
                    parts = os.path.normpath(suite_base_path).split(os.sep)
                    try:
                        basepath_list = parts[parts.index('src') + 1:]
                    except ValueError:
                        basepath_list = []
                    contains_apis_or_ui = False
                    for item in basepath_list:
                        for keyword in ['apis', 'ui', 'val', 'src']:
                            if keyword in item:
                                contains_apis_or_ui = True
                                break
                        if contains_apis_or_ui:
                            break
                    if contains_apis_or_ui:
                        path_list = basepath_list[-3:]
                    else:
                        path_list = basepath_list[-2:]
                    base_path = os.path.join(*path_list)
                    print('Default basepath : %s' % base_path)
                suite_network_logger = suite.get('network_logger', self.network_logger_enabled)
                network_logger_env = 'TRUE' if str(suite_network_logger).upper() in ('TRUE', 'YES') else 'FALSE'
                # get the name of the suite
                if 'suitefile' in suite:
                    name = suite['suitefile']
                    # unique name for each container
                    random_number = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(8)])
                    container_name = name + '_' + random_number
                else:
                    name = suite['suitedirectory']

                print('- Starting container: %s for suite: %s' % (container_name, name))

                # get agency for this suite
                if 'agency' in suite:
                    agency = suite['agency']
                else:
                    agency = self.agency

                if 'robottags' in suite:
                    robottags = suite['robottags']
                else:
                    robottags = self.robot_tags

                # run strategy
                if 'reruntests' in suite:
                    reruntests = suite['reruntests']
                else:   reruntests = self.reruntests

                # get testcases for this suite
                if 'testcases' in suite:
                    testcases = suite['testcases']
                else:
                    testcases = 'NotSet'

                # launch the container
                try:
                    container = docker_client.containers.run(
                        image=ParallelRunner.IMAGE_NAME,
                        auto_remove=True,
                        detach=True,
                        shm_size='512m',
                        volumes={self.output_path: {'bind': '/home/seluser/share', 'mode': 'rw'}},
                        environment={"SUITE": name, "SHARE": "/home/seluser/share", "AGENCY": agency,
                                     "TESTCASES": testcases, "BASEPATH": base_path, "TAGS": robottags,
                                     "REPORTTITLE": self.report_title,
                                     "TESTRAILRUNIDENABLE": self.test_rail_result_enabled,
                                     "TESTRAILPROJECTID": self.testrail_project_id,
                                     "TESTRAILSUITEID": self.testrail_suite_id,
                                     "CIVPVERSION": self.civic_platform_version,
                                     "JENKINSBUILDNUMBER": self.jenkins_build_number,
                                     "RERUNTESTS": reruntests,
                                     "BROWSER_LOCATION": self.browser_location,
                                     "BROWSER_CAPABILITIES": self.desired_capabilities,
                                     "ZAP_ENABLED": self.zap_enabled,  "PROXY_HOST": self.proxy_host,
                                     "PROXY_PORT": self.proxy_port, "PROXY_PATH": self.proxy_path,
                                     "NETWORK_LOGGER_ENABLED": network_logger_env},
                        name=container_name,
                        extra_hosts=extra_hosts_entries,
                        entrypoint=ParallelRunner.ENTRY_SCRIPT
                    )
                    print('- Container started for suite : %s' % name)
                    containers.append(container)
                except Exception as e:
                    print('- Error launching container for suite: %s - error: %s' % (name, str(e)))
            for container in containers:
                print('- Waiting for container: %s' % container.name)
                try:
                    container.wait(timeout=18000)
                    print('- Container for suite: %s, has completed' % container.name)
                except Exception as e:
                    print('- Error determining if container for suite %s has completed' % container.name)
                    print('Exception: ' + str(e))
        except Exception as e:
            print('- Error launching containers: %s' % str(e))

    def __copy_files_to_jenkins_workspace(self):
        """Copies the per suite junit and annotation files to jenkins workspace"""
        if Jenkins.is_running_from_jenkins():
            print('Copying files back to jenkins workspace')
            workspace = Jenkins.get_workspace()
            for suite in self.execution_file_json['suites']:
                if 'suitefile' in suite:
                    name = suite['suitefile'].replace('.robot', '')
                else:
                    name = suite['suitedirectory']
                try:
                    output_junit_path = os.path.join(self.output_path, name,
                                                     ParallelRunner.JUNIT_XML.replace('SUITE', name))
                    destination_path = os.path.join(workspace, ParallelRunner.JUNIT_XML.replace('SUITE', name))
                    print('- Copying junit file: %s' % output_junit_path)
                    shutil.copyfile(src=output_junit_path, dst=destination_path)
                    output_annotations_path = os.path.join(self.output_path, name,
                                                           ParallelRunner.ANNOTATIONS_JSON.replace('SUITE', name))
                    destination_path = os.path.join(workspace, ParallelRunner.ANNOTATIONS_JSON.replace('SUITE', name))
                    print('- Copying annotations file: %s' % output_annotations_path)
                    shutil.copyfile(src=output_annotations_path, dst=destination_path)
                except Exception as e:
                    print('- Error copying file:', e)
            print('- All suite file copied')
            print('- Copying html report files')
            try:
                html_log_path = os.path.join(self.output_path, ParallelRunner.HTML_LOG)
                destination_path = os.path.join(workspace, ParallelRunner.HTML_LOG)
                print('- Copying html log file: %s' % html_log_path)
                shutil.copyfile(src=html_log_path, dst=destination_path)
                html_report_path = os.path.join(self.output_path, ParallelRunner.HTML_REPORT)
                destination_path = os.path.join(workspace, ParallelRunner.HTML_REPORT)
                print('- Copying html report file: %s' % html_report_path)
                shutil.copyfile(src=html_report_path, dst=destination_path)
                xml_report_path = os.path.join(self.output_path, ParallelRunner.XML_REPORT)
                destination_path = os.path.join(workspace, ParallelRunner.XML_REPORT)
                print('- Copying xml junit report file: %s' % xml_report_path)
                shutil.copyfile(src=xml_report_path, dst=destination_path)
            except Exception as e:
                print('- Error copying html,xml files:', e)
        else:
            print('No files to copy to Jenkins workspace')

    def __read_extra_hosts(self):
        """Reads extrahosts.txt file to build out container /etc/hosts file custom entries"""
        try:
            suite_base_path = self.suite_base_path
            parts = os.path.normpath(suite_base_path).split(os.sep)
            try:
                basepath_list = parts[parts.index('src') + 1:]
            except ValueError:
                basepath_list = []
            contains_apis_or_ui = False
            for item in basepath_list:
                for keyword in ['apis', 'ui', 'val', 'src']:
                    if keyword in item:
                        contains_apis_or_ui = True
                        break
                if contains_apis_or_ui:
                    break
            if 'internet' in self.suite_base_path:
                full_file_path = self.suite_base_path + '/../../extrahosts.txt'
            elif contains_apis_or_ui:
                full_file_path = self.suite_base_path + '/../../../../extrahosts.txt'
            else:
                full_file_path = self.suite_base_path + '/../../../extrahosts.txt'
                
            if not os.path.exists(full_file_path) or not os.stat(full_file_path).st_size > 0:
                raise Exception('extrahosts.txt file was not found at %s or file is empty' % full_file_path)
            print('Reading extra hosts file: %s' % full_file_path)
            with open(full_file_path, "r", encoding="utf-8", errors="replace") as hosts_file:
                return self.__build_extra_hosts_dict(hosts_file)
        except OSError as err:
            print('Open extra hosts file failed' + str(err))
            raise
        except Exception as e:
            print('Exception parsing extrahosts.txt file : %s' % e)
            raise

    def __is_test_dir(self, root):
        return any(root.endswith(pattern) for pattern in test_dirs)

    @staticmethod
    def __build_extra_hosts_dict(hosts):
        """Populates and returns an extra_hosts dictionary"""
        try:
            extra_hosts_dict = {}
            for host in hosts:
                host = host.strip()
                if not host or host.startswith('#'):
                    continue
                hostname = host.split()[1].strip()
                ipaddress = host.split()[0].strip()
                extra_hosts_dict.update({hostname: ipaddress})
            return extra_hosts_dict
        except Exception as e:
            raise Exception('Error parsing lines of extrahosts.txt file: %s' % e)

    def __build_dynamic_suite_file(self):
        """builds a suite file dynamically across all application directories"""
        if self.confluence_attachment_name:
            print('- Need to exclude some test cases based on this confluence attachment: %s' % self.confluence_attachment_name)
            self.confluence_json_file = self.__get_json_from_attachment(self, self.confluence_attachment_name)
            if not self.confluence_json_file:
                print('- Error: Unable to build json data object from Confluence attachment named: %s' % self.confluence_attachment_name)
                raise SystemExit()
        else:
            print('- No confluence attachment file specified so will run full set of CICD test cases')
        suite_list = []
        try:
            root_dir = './src'
            for root, dirs, files in os.walk(root_dir):
                if self.__is_test_dir(root):
                    for file_name in files:
                        full_file_path = os.path.join(root, file_name)
                        if self.confluence_attachment_name and file_name.endswith('.robot'):
                            test_case_ids_to_search = self.__get_test_case_ids_to_search(self.confluence_json_file)
                            file_path = full_file_path
                            robot_file_content = None
                            with open(file_path, 'r') as robot_file:
                                robot_file_content = robot_file.read()
                            test_case_ids_in_file = self.__get_test_case_ids(robot_file_content)
                            matching_test_case_ids = test_case_ids_in_file.intersection(test_case_ids_to_search)
                            if matching_test_case_ids:
                                base_path = self.__get_base_path(root)
                                formatted_test_case_ids = self.__format_test_case_ids(list(matching_test_case_ids))
                                suite_list.append('{ "basepath": "%s", "suitefile": "%s", "robottags": "-i %s" },' % (
                                base_path, file_name, formatted_test_case_ids))
                                self.confluence_json_file['testRailIds'] = [test_case for test_case in self.confluence_json_file['testRailIds'] if test_case['id'] not in matching_test_case_ids]
                        else:
                            if file_name.endswith('.robot') and self.__contains_tag(full_file_path):
                                list_of_testcases = self.__get_tagged_testcase_names(self, full_file_path)
                                base_path = self.__get_base_path(root)
                                formatted_test_cases = self.__create_formatted_testcase_list(list_of_testcases)
                                suite_list.append('{ "basepath": "%s", "suitefile": "%s", "testcases": "%s" },' % (
                                    base_path, file_name, formatted_test_cases))
            return self.__create_formatted_suite_file(suite_list)
        except Exception as e:
            print('Exception creating suite list object : %s' % e)
            raise

    @staticmethod
    def __contains_tag(file_path):
        """tests that file contains expected tag or returns False"""
        try:
            for line in open(file_path).readlines():
                if '[TAGS]' in line.upper() and 'CICD:YES' in line.upper():
                    return True
            return False
        except Exception as e:
            print('Exception opening file : %s' % e)
            raise

    def __get_tagged_testcase_names(self, file_path):
        """Returns a list of tagged test case names"""
        testcase_list = []
        try:
            file_lines = []
            with open(file_path, "r", encoding="utf-8") as f:
                file_lines = f.readlines()
            for index, line in enumerate(file_lines):
                if '[TAGS]' in line.upper() and 'CICD:YES' in line.upper():
                    if self.confluence_attachment_name:
                        if self.__verify_test_case_not_excluded(self, line):
                            testcase_list.append(file_lines[index - 2].strip())
                    else:
                        testcase_list.append(file_lines[index - 2].strip())
            return testcase_list
        except Exception as e:
            print('Exception handling file : %s' % e)
            raise

    @staticmethod    
    def __get_test_case_ids(robot_file_content):
        pattern = r'TestRail:(\w+)'
        matches = re.findall(pattern, robot_file_content)
        return set(matches)
    
    @staticmethod    
    def __get_test_case_ids_to_search(json_file):
        test_case_ids_to_search = set()
        for test_case in json_file['testRailIds']:
            if test_case['execute'] == 'Y':
                test_case_ids_to_search.add(test_case['id'])
        return test_case_ids_to_search

    @staticmethod
    def __format_test_case_ids(test_case_ids):
        if len(test_case_ids) == 1:
            return f"TestRail:{test_case_ids[0]}"
        elif len(test_case_ids) > 1:
            return "OR".join(f"TestRail:{test_case_id}" for test_case_id in test_case_ids)
        else:
            return ""

    @staticmethod
    def __verify_test_case_not_excluded(self, tagged_line):
        testrailid_list = re.findall("C[0-9]+", tagged_line)
        for testrailid in testrailid_list:
            if self.__check_test_case_enabled(self.confluence_json_file, testrailid):
                return True
        print("- Unable to find the CICD testrailid(s) in the confluence attachment: %s" % testrailid_list)
        return False

    @staticmethod
    def __get_base_path(root_dir):
        """Resolves the base path that the docker container run command needs"""
        parts = os.path.normpath(root_dir).split(os.sep)
        try:
            basepath_list = parts[parts.index('src') + 1:]
            return os.path.join(*basepath_list)
        except Exception as e:
            print('Exception parsing base path : %s' % e)
            raise

    @staticmethod
    def __create_formatted_testcase_list(list_of_testcases):
        """Creates a formatted list of test cases from list of test cases from a single suite"""
        formatted_test_cases = ''
        for index, test_name in enumerate(list_of_testcases):
            formatted_test_cases = formatted_test_cases + test_name + ","
            if index == len(list_of_testcases) - 1:
                formatted_test_cases = formatted_test_cases[:-1]
        return formatted_test_cases

    @staticmethod
    def __create_formatted_suite_file(suite_list):
        """Creates a formatted suite json file in memory that defines this test run"""
        try:
            formatted_suite_file = ''
            formatted_suite_file = formatted_suite_file + '{'
            formatted_suite_file = formatted_suite_file + '"suites": ['
            for index, line in enumerate(suite_list):
                if index == len(suite_list) - 1:
                    line = line[:-1]
                formatted_suite_file = formatted_suite_file + line
            formatted_suite_file = formatted_suite_file + ']'
            formatted_suite_file = formatted_suite_file + '}'
            formatted_suite_file = json.loads(formatted_suite_file)
        except Exception as e:
            print('Exception creating suite file : %s' % e)
            raise
        return formatted_suite_file

    @staticmethod
    def __get_json_from_attachment(self, confluence_file_name):
        try:
            confluence = self.__get_confluence_object()
            attachments = self.__get_confluence_attachments(confluence)
            for attachment in attachments:
                if attachment['title'] == confluence_file_name + '.json':
                    response = requests.get(confluence.url + attachment['_links']['download'],
                                            auth=(confluence.username, confluence.password))
                    if response.status_code == 200:
                        return response.json()
                else:
                    continue
        except requests.exceptions.RequestException as e:
            raise SystemExit(e)

    @staticmethod
    def __get_confluence_object():
        try:
            accela_url = 'https://accelaeng.atlassian.net/'
            user = 'dvu@accela.com'
            key = 'ATATT3xFfGF0N1h3BU7EJXFQWq4-co0bD-C737glZGChT3teGBP21KP73WWPnODSSo_XF7464ZwEOwW5CgqYKAnghibDsH4HHZIN3PgjqgYc8QhNXmJWQP3VnsjoIv8PuJYR2gFefZdkHdcGZsxywk8AghjHEWfMbgl5O0B8iiQndn_gr68kXLE=A5693CCB'
            return Confluence(url=accela_url, username=user,
                              password=key)
        except requests.exceptions.ConnectionError as conn_err:
            raise SystemExit(conn_err)
        except requests.exceptions.RequestException as e:
            raise SystemExit(e)

    @staticmethod
    def __get_confluence_attachments(confluence):
        try:
            attachments_container = confluence.get_attachments_from_content(page_id=2038988999, start=0, limit=500)
            attachments = attachments_container['results']
            return attachments
        except requests.exceptions.RequestException as e:
            raise SystemExit(e)

    @staticmethod
    def __check_test_case_enabled(json_data, testrailid):
        for index, key in enumerate(json_data['testRailIds']):
            if key['id'] == testrailid and key['execute'] == 'Y':
                return True
        return False

    @staticmethod
    def __handle_suspend_request(self):
        """Handles an external request to temporarily suspend execution"""
        polling_period = 60.0 #60 seconds
        maximum_suspend_minutes = 180  #3 hours
        suspend_file_name = "SUSPENDME"
        suspend_file_path = os.path.join(self.output_path, suspend_file_name)
        if os.path.exists(suspend_file_path):
            print("Suspend file exists, entering polling loop for a maximum of %s minutes...." % maximum_suspend_minutes)
            for i in range(maximum_suspend_minutes):
                time.sleep(polling_period)
                if os.path.exists(suspend_file_path):
                    print("*")
                else:
                    print("Suspend file removed, continuing execution...")
                    return
            print("Polled for maximum allowed time - continuing execution...")
            return
        else:
            return
