import logging
import pprint
import testrail

from argparse import ArgumentParser
from datetime import datetime
from pathlib import Path
from lxml import etree


def configure_logging():
    """
    This method configures the logging
    """
    log_format = '%(asctime)-15s %(levelname)-10s %(message)s'
    Path("./output/log").mkdir(parents=True, exist_ok=True)
    logging.basicConfig(filename='./output/log/robotframework2testrail.log', format=log_format, level=logging.DEBUG)
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(logging.Formatter('%(message)s'))
    logging.getLogger().addHandler(console_handler)


class RobotTestrailReporter(object):

    def __init__(self):
        configure_logging()
        self.rf_to_tr_status = {
            "PASS": 1,
            "FAIL": 5,
        }
        self.tr_to_rf_status = {
            1: "PASS",
            5: "FAIL"
        }
        self.time_format = '%Y%m%d %H:%M:%S.%f'
        self.comment_size_limit = 1000

    def find_tests_tagged_by(self, path, tag):
        """
        This method parses the output xml generated by robotframework
        defined by ``path`` and finds all test nodes with starting tag ``tag``.
        Returns a list of dictionaries with following keys:
        ['rf_test_name', 'test_id , 'status_id, 'comment', 'elapsed']
            - ``rf_test_name`` is the robotframework test name
            - ``test_id`` is the numeric value contained in tag
            - ``status_id`` is the conversion of test status to testrail standards
            - ``comment`` is the test status message
            - ``elapsed`` is the test execution time in s, (min 1 second)
        Args:
            ``path`` is the path of robotframework xml output file
            ``tag`` is the tag to be found in robotframework tests, it
            must contain a numeric part which specifies testcase run id, 
            example: test_case_id=123

        """
        tree = etree.parse(path)
        tests = tree.xpath(".//tags/tag[starts-with(., '%s')]/ancestor::test" % tag)
        results = []
        for test in tests:
            test_id = test.xpath("tags/tag[starts-with(., '%s')]" % tag)[0].text
            test_id = ''.join([d if d.isdigit() else '' for d in test_id])
            status = test.find('status')
            elapsed = self.get_elapsed(status)
            comment = self.get_comment(status)
            result = {
                'rf_test_name': test.get('name'),
                'test_id': int(test_id),
                'status_id': self.rf_to_tr_status[status.get('status')],
                'comment': comment,
                'elapsed': elapsed
            }
            results.append(result)
        return results

    @staticmethod
    def publish_results(api, run_id, results):
        """
        This method publishes robotframework results to a testrail run

        Args:
            ``api`` is an instance of testrail.client.API already logged in
            ``run_id`` is the id of the run to update
            ``results`` is a list of dictionaries that contains test results
        """
        api.add_results(results, run_id)

    @staticmethod
    def replace_caseid_with_testid(api, results, run_id):
        """
        This method parse all results generated by find_tests_tagged_by method
        and replace all testcases ids with specific testruns ids in ``test_id`` key
        of each result
        Args:
            ``api`` is an instance of testrail.client.API already logged in
            ``results`` is a list of dictionaries that contains test results
            ``run_id`` is the id of the run to update
        """
        tests = api.tests(run_id)
        mapping = {}
        results_to_return = results.copy()
        for test in tests:
            mapping[test['case_id']] = test['id']
        for result in results:
            if result['test_id'] in mapping:
                result['test_id'] = mapping[result['test_id']]
            else:
                results_to_return.remove(result)
                logging.error("Test Case: %s with tag: %s not present in Run: %s" % (
                    result['rf_test_name'], result['test_id'], run_id))
        return results_to_return

    def get_elapsed(self, status):
        """
        This method calculates the elapsed test execution time which is
        endtime - starttime in test ``status`` node of output XML.
        Returns a string of test execution seconds. Example: '5s'
        Args:
            ``status`` is the status node found via xpath selector with lxml
        """
        start_time = status.get('starttime')
        end_time = status.get('endtime')
        elapsed = datetime.strptime(end_time + '000', self.time_format) - datetime.strptime(start_time + '000',
                                                                                            self.time_format)
        elapsed = round(elapsed.total_seconds())
        elapsed = 1 if (elapsed < 1) else elapsed  # TestRail API doesn't manage msec (min value=1s)
        elapsed = "%ss" % elapsed
        return elapsed

    def get_comment(self, status):
        """
        This method returns the test execution ``status`` message which is the
        text attribute of test status node in output xml, if no text is found
        it retuns an empty string. Comment maximum length is defined by
        comment_size_limit variable.
        Args:
            ``status`` is the status node found via xpath selector with lxml
        """
        comment = '' if status.text is None else status.text
        if comment:
            comment = "# Robot Framework result: #\n    " + comment[:self.comment_size_limit].replace('\n', '\n    ')
            comment += '\n...\nLog truncated' if len(str(comment)) > self.comment_size_limit else ''
        return comment


def main():
    """
    Main method to execute, it reads following arguments from cli:
      -f --input  => xml file to parse
      -u --url    => testrail url
      -e --email  => testrail email
      -k --key    => testrail password or apikey
      -r --runid  => testrail runid to update
      -t --tag    => tag to be found in robotframework tests, default="test_case_id="
    """
    parser = ArgumentParser()
    parser.add_argument("-f",
                        "--input",
                        action="store",
                        type=str,
                        dest="input_file",
                        help="Path to the XML report to parse",
                        required=True)
    parser.add_argument("-u",
                        "--url",
                        action="store",
                        type=str,
                        dest="url",
                        help="Testrail url",
                        required=True)
    parser.add_argument("-k",
                        "--key",
                        action="store",
                        type=str,
                        dest="key",
                        help="Password or apikey",
                        required=True)
    parser.add_argument("-e",
                        "--email",
                        action="store",
                        type=str,
                        dest="email",
                        help="Email address",
                        required=True)
    parser.add_argument("-r",
                        "--runid",
                        action="store",
                        type=str,
                        dest="runid",
                        help="Testrail run id",
                        required=True)
    parser.add_argument("-t",
                        "--tag",
                        action="store",
                        type=str,
                        dest="tag",
                        help="Tag prefix to insert in RF tests",
                        default="test_case_id=")
    args = parser.parse_args()
    url = args.url
    email = args.email
    key = args.key
    tr_run_id = args.runid
    input_file = args.input_file
    tag = args.tag
    pp = pprint.PrettyPrinter()
    tr_api = testrail.client.API(email=email, key=key, url=url)
    reporter = RobotTestrailReporter()
    res = reporter.find_tests_tagged_by(input_file, tag)
    logging.debug(pp.pformat(res))
    res = reporter.replace_caseid_with_testid(api=tr_api, results=res, run_id=tr_run_id)
    logging.debug(pp.pformat(res))
    if res:
        reporter.publish_results(api=tr_api, run_id=tr_run_id, results=res)
    res_to_print = ["Test name: %s, Test id: %s, Status: %s" %
                    (result['rf_test_name'], result['test_id'], reporter.tr_to_rf_status[result['status_id']]) for
                    result in res]
    logging.info("\n".join(res_to_print))


if __name__ == '__main__':
    main()
