# -*- coding: utf-8 -*-

from typing import List, Optional

from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError, Future
from requests.exceptions import RequestException
from robot.api import SuiteVisitor, TestSuite
from robot.output import LOGGER
from TestRailAPIClient import TestRailAPIClient, TESTRAIL_STATUS_ID_PASSED

CONNECTION_TIMEOUT = 60  # Value in seconds of timeout connection with testrail for one request


class TestRailPreRunModifier(SuiteVisitor):
    """Pre-run modifier for starting test cases from a certain test run in [http://www.gurock.com/testrail/ | TestRail].

    == Dependencies ==
    | robot framework | http://robotframework.org |
    | TestRailAPIClient |

    == Preconditions ==
    1. [ http://docs.gurock.com/testrail-api2/introduction | Enable TestRail API] \n

    == Example ==
    1. Create test cases in TestRail with case_id: 10,11,12. \n
    2. Add test cases with case_id:10,12 into test run with run_id = 20. \n
    3. Create robot_suite in Robot Framework:
    | *** Test Cases ***
    | Autotest name 1
    |     [Documentation]    Autotest 1 documentation
    |     [Tags]    testrailid=10
    |     Fail    Test fail message
    | Autotest name 2
    |     [Documentation]    Autotest 2 documentation
    |     [Tags]    testrailid=11
    |     Fail    Test fail message
    | Autotest name 3
    |     [Documentation]    Autotest 3 documentation
    |     [Tags]    testrailid=12
    |     Fail    Test fail message
    4. Run Robot Framework with pre-run modifier:
    | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:0 robot_suite.robot
    5. Test cases "Autotest name 1" and "Autotest name 3" will be executed. Test case "Autotest name 2" will be skipped.
    6. To execute tests from TestRail test run only with a certain status, for example "failed" and "blocked":
    | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:0:failed:blocked robot_suite.robot
    6. To execute stable tests from TestRail test run with run analysis depth = 5:
    | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:5 robot_suite.robot
    """

    def __init__(self, server: str, user: str, password: str, run_id: str, protocol: str,  # noqa: E951
                 results_depth: str, *status_names: str) -> None:
        """Pre-run modifier initialization.

        *Args:*\n
            _server_ - name of TestRail server;\n
            _user_ - name of TestRail user;\n
            _password_ - password of <PASSWORD>;\n
            _run_id_ - ID of the test run;\n
            _protocol_ - connecting protocol to TestRail server: http or https;\n
            _results_depth_ - analysis depth of run results;\n
            _status_names_ - name of test statuses in TestRail.
        """
        self.run_id = run_id
        self.status_names = status_names
        self.tr_client = TestRailAPIClient(server, user, password, run_id, protocol)
        self.results_depth = int(results_depth) if str(results_depth).isdigit() else 0
        self._tr_tags_list: Optional[List[str]] = None
        self._tr_stable_tags_list: Optional[List[str]] = None
        LOGGER.register_syslog()

    @property
    def tr_stable_tags_list(self) -> List[str]:
        """Gets list of 'testrailid' tags of the stable test cases.

        Returns:
            List of tags.
        """
        if self._tr_stable_tags_list is None:
            self._tr_stable_tags_list = self._get_tr_stable_tags_list()

        return self._tr_stable_tags_list

    @property
    def tr_tags_list(self) -> List[str]:
        """Gets 'testrailid' tags.

        Returns:
            List of tags.
        """
        if self._tr_tags_list is None:
            self._tr_tags_list = self._get_tr_tags_list()

        return self._tr_tags_list

    def _log_to_parent_suite(self, suite: TestSuite, message: str) -> None:
        """Log message to the parent suite.

        *Args:*\n
            _suite_ - Robot Framework test suite object.
            _message_ - message.
        """
        if suite.parent is None:
            LOGGER.error("{suite}: {message}".format(suite=suite, message=message))

    def _get_tr_tags_list(self) -> List[str]:
        """Get list of 'testrailid' tags.

        If required test statuses from the test run are passed to modifier,
        a request is made to the TestRail to obtain information about all the statuses.
        Theirs identifiers will be retrieved from list of all the statuses.
        This identifiers will be used to receive test tags in the required status.

        If statuses aren't passed to modifier,
        the tags of all tests in the test run will be obtained regardless of their status.

        Returns:
            List of tags.
        """
        status_ids = None
        if self.status_names:
            status_ids = [self.tr_client.get_status_id_by_status_label(name) for name in self.status_names]
        tests_info = self.tr_client.get_tests(run_id=self.run_id, status_ids=status_ids)
        return ['testrailid={}'.format(test["case_id"]) for test in tests_info if test["case_id"] is not None]

    def _get_tr_stable_tags_list(self) -> List[str]:
        """Get list of 'testrailid' tags of the stable test cases.

        If analysis depth of the run results is passed to modifier and its value greater than zero,
        a request is made to the TestRail to receive information about test cases whose last result is 'passed'.
        Based on the information received, the results of the latest runs for these test cases are analyzed,
        on the basis of which the tags of stable test cases will be received.

        Returns:
            List of stable tags.
        """
        stable_case_ids_list = list()
        catched_exceptions = list()
        passed_tests_info = self.tr_client.get_tests(run_id=self.run_id, status_ids=[TESTRAIL_STATUS_ID_PASSED])
        case_ids = [test["case_id"] for test in passed_tests_info if test["case_id"] is not None]

        def future_handler(future: Future) -> None:
            """Get result from future with try/except block and to list.
            Args:
                future: future object.
            """
            case_id = futures[future]
            try:
                case_results = future.result()
            except RequestException as exception:
                catched_exceptions.append(exception)
            else:
                passed_list = [result for result in case_results if
                               result['status_id'] == TESTRAIL_STATUS_ID_PASSED]
                if len(passed_list) == int(self.results_depth):
                    stable_case_ids_list.append(case_id)

        with ThreadPoolExecutor() as executor:
            futures = {executor.submit(self.tr_client.get_results_for_case, self.run_id, case_id, self.results_depth):
                       case_id for case_id in case_ids}

            for future in as_completed(futures, timeout=CONNECTION_TIMEOUT):
                future_handler(future)

        if catched_exceptions:
            raise catched_exceptions[0]
        return ['testrailid={}'.format(case_id) for case_id in stable_case_ids_list]

    def start_suite(self, suite: TestSuite) -> None:
        """Form list of tests for the Robot Framework test suite that are included in the TestRail test run.

        If analysis depth of the run results is greater than zero, when first suite is launched
        a list of 'testrailid' tags of stable test cases is obtained.
        After that the list of tags is written to the class attribute and for subsequent suites the obtaining is not happening.

        If analysis depth of the run results is zero, when the first suite is launched
        a list of 'testrailid' tags of all test cases in the given status is obtained.
        After that the list of tags is written to the class attribute and for subsequent suites the obtaining is not happening.

        *Args:*\n
            _suite_ - Robot Framework test suite object.
        """
        tests = suite.tests
        suite.tests = None
        try:
            if self.results_depth > 0:
                suite.tests = [t for t in tests if (set(t.tags) & set(self.tr_stable_tags_list))]
            else:
                suite.tests = [t for t in tests if (set(t.tags) & set(self.tr_tags_list))]
        except (RequestException, TimeoutError) as error:
            self._log_to_parent_suite(suite, str(error))

    def end_suite(self, suite: TestSuite) -> None:
        """Removing test suites that are empty after excluding tests that are not part of the TestRail test run.

        *Args:*\n
            _suite_ - Robot Framework test suite object.
        """
        suite.suites = [s for s in suite.suites if s.test_count > 0]
        if not suite.suites:
            self._log_to_parent_suite(suite, "No tests to execute after using TestRail pre-run modifier.")

# -*- coding: utf-8 -*-

from typing import List, Optional

from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError, Future
from requests.exceptions import RequestException
from robot.api import SuiteVisitor, TestSuite
from robot.output import LOGGER
from TestRailAPIClient import TestRailAPIClient, TESTRAIL_STATUS_ID_PASSED

CONNECTION_TIMEOUT = 60  # Value in seconds of timeout connection with testrail for one request


class TestRailPreRunModifier(SuiteVisitor):
    """Pre-run modifier for starting test cases from a certain test run in [http://www.gurock.com/testrail/ | TestRail].

    == Dependencies ==
    | robot framework | http://robotframework.org |
    | TestRailAPIClient |

    == Preconditions ==
    1. [ http://docs.gurock.com/testrail-api2/introduction | Enable TestRail API] \n

    == Example ==
    1. Create test cases in TestRail with case_id: 10,11,12. \n
    2. Add test cases with case_id:10,12 into test run with run_id = 20. \n
    3. Create robot_suite in Robot Framework:
    | *** Test Cases ***
    | Autotest name 1
    |     [Documentation]    Autotest 1 documentation
    |     [Tags]    testrailid=10
    |     Fail    Test fail message
    | Autotest name 2
    |     [Documentation]    Autotest 2 documentation
    |     [Tags]    testrailid=11
    |     Fail    Test fail message
    | Autotest name 3
    |     [Documentation]    Autotest 3 documentation
    |     [Tags]    testrailid=12
    |     Fail    Test fail message
    4. Run Robot Framework with pre-run modifier:
    | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:0 robot_suite.robot
    5. Test cases "Autotest name 1" and "Autotest name 3" will be executed. Test case "Autotest name 2" will be skipped.
    6. To execute tests from TestRail test run only with a certain status, for example "failed" and "blocked":
    | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:0:failed:blocked robot_suite.robot
    6. To execute stable tests from TestRail test run with run analysis depth = 5:
    | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:5 robot_suite.robot
    """

    def __init__(self, server: str, user: str, password: str, run_id: str, protocol: str,  # noqa: E951
                 results_depth: str, *status_names: str) -> None:
        """Pre-run modifier initialization.

        *Args:*\n
            _server_ - name of TestRail server;\n
            _user_ - name of TestRail user;\n
            _password_ - password of <PASSWORD>;\n
            _run_id_ - ID of the test run;\n
            _protocol_ - connecting protocol to TestRail server: http or https;\n
            _results_depth_ - analysis depth of run results;\n
            _status_names_ - name of test statuses in TestRail.
        """
        self.run_id = run_id
        self.status_names = status_names
        self.tr_client = TestRailAPIClient(server, user, password, run_id, protocol)
        self.results_depth = int(results_depth) if str(results_depth).isdigit() else 0
        self._tr_tags_list: Optional[List[str]] = None
        self._tr_stable_tags_list: Optional[List[str]] = None
        LOGGER.register_syslog()

    @property
    def tr_stable_tags_list(self) -> List[str]:
        """Gets list of 'testrailid' tags of the stable test cases.

        Returns:
            List of tags.
        """
        if self._tr_stable_tags_list is None:
            self._tr_stable_tags_list = self._get_tr_stable_tags_list()

        return self._tr_stable_tags_list

    @property
    def tr_tags_list(self) -> List[str]:
        """Gets 'testrailid' tags.

        Returns:
            List of tags.
        """
        if self._tr_tags_list is None:
            self._tr_tags_list = self._get_tr_tags_list()

        return self._tr_tags_list

    def _log_to_parent_suite(self, suite: TestSuite, message: str) -> None:
        """Log message to the parent suite.

        *Args:*\n
            _suite_ - Robot Framework test suite object.
            _message_ - message.
        """
        if suite.parent is None:
            LOGGER.error("{suite}: {message}".format(suite=suite, message=message))

    def _get_tr_tags_list(self) -> List[str]:
        """Get list of 'testrailid' tags.

        If required test statuses from the test run are passed to modifier,
        a request is made to the TestRail to obtain information about all the statuses.
        Theirs identifiers will be retrieved from list of all the statuses.
        This identifiers will be used to receive test tags in the required status.

        If statuses aren't passed to modifier,
        the tags of all tests in the test run will be obtained regardless of their status.

        Returns:
            List of tags.
        """
        status_ids = None
        if self.status_names:
            status_ids = [self.tr_client.get_status_id_by_status_label(name) for name in self.status_names]
        tests_info = self.tr_client.get_tests(run_id=self.run_id, status_ids=status_ids)
        return ['testrailid={}'.format(test["case_id"]) for test in tests_info if test["case_id"] is not None]

    def _get_tr_stable_tags_list(self) -> List[str]:
        """Get list of 'testrailid' tags of the stable test cases.

        If analysis depth of the run results is passed to modifier and its value greater than zero,
        a request is made to the TestRail to receive information about test cases whose last result is 'passed'.
        Based on the information received, the results of the latest runs for these test cases are analyzed,
        on the basis of which the tags of stable test cases will be received.

        Returns:
            List of stable tags.
        """
        stable_case_ids_list = list()
        catched_exceptions = list()
        passed_tests_info = self.tr_client.get_tests(run_id=self.run_id, status_ids=[TESTRAIL_STATUS_ID_PASSED])
        case_ids = [test["case_id"] for test in passed_tests_info if test["case_id"] is not None]

        def future_handler(future: Future) -> None:
            """Get result from future with try/except block and to list.
            Args:
                future: future object.
            """
            case_id = futures[future]
            try:
                case_results = future.result()
            except RequestException as exception:
                catched_exceptions.append(exception)
            else:
                passed_list = [result for result in case_results if
                               result['status_id'] == TESTRAIL_STATUS_ID_PASSED]
                if len(passed_list) == int(self.results_depth):
                    stable_case_ids_list.append(case_id)

        with ThreadPoolExecutor() as executor:
            futures = {executor.submit(self.tr_client.get_results_for_case, self.run_id, case_id, self.results_depth):
                       case_id for case_id in case_ids}

            for future in as_completed(futures, timeout=CONNECTION_TIMEOUT):
                future_handler(future)

        if catched_exceptions:
            raise catched_exceptions[0]
        return ['testrailid={}'.format(case_id) for case_id in stable_case_ids_list]

    def start_suite(self, suite: TestSuite) -> None:
        """Form list of tests for the Robot Framework test suite that are included in the TestRail test run.

        If analysis depth of the run results is greater than zero, when first suite is launched
        a list of 'testrailid' tags of stable test cases is obtained.
        After that the list of tags is written to the class attribute and for subsequent suites the obtaining is not happening.

        If analysis depth of the run results is zero, when the first suite is launched
        a list of 'testrailid' tags of all test cases in the given status is obtained.
        After that the list of tags is written to the class attribute and for subsequent suites the obtaining is not happening.

        *Args:*\n
            _suite_ - Robot Framework test suite object.
        """
        tests = suite.tests
        suite.tests = None
        try:
            if self.results_depth > 0:
                suite.tests = [t for t in tests if (set(t.tags) & set(self.tr_stable_tags_list))]
            else:
                suite.tests = [t for t in tests if (set(t.tags) & set(self.tr_tags_list))]
        except (RequestException, TimeoutError) as error:
            self._log_to_parent_suite(suite, str(error))

    def end_suite(self, suite: TestSuite) -> None:
        """Removing test suites that are empty after excluding tests that are not part of the TestRail test run.

        *Args:*\n
            _suite_ - Robot Framework test suite object.
        """
        suite.suites = [s for s in suite.suites if s.test_count > 0]
        if not suite.suites:
            self._log_to_parent_suite(suite, "No tests to execute after using TestRail pre-run modifier.")

# -*- coding: utf-8 -*- # Value in seconds of timeout connection with testrail for one request Pre-run modifier for starting test cases from a certain test run in [http://www.gurock.com/testrail/ | TestRail].      == Dependencies ==     | robot framework | http://robotframework.org |     | TestRailAPIClient |      == Preconditions ==     1. [ http://docs.gurock.com/testrail-api2/introduction | Enable TestRail API] \n      == Example ==     1. Create test cases in TestRail with case_id: 10,11,12. \n     2. Add test cases with case_id:10,12 into test run with run_id = 20. \n     3. Create robot_suite in Robot Framework:     | *** Test Cases ***     | Autotest name 1     |     [Documentation]    Autotest 1 documentation     |     [Tags]    testrailid=10     |     Fail    Test fail message     | Autotest name 2     |     [Documentation]    Autotest 2 documentation     |     [Tags]    testrailid=11     |     Fail    Test fail message     | Autotest name 3     |     [Documentation]    Autotest 3 documentation     |     [Tags]    testrailid=12     |     Fail    Test fail message     4. Run Robot Framework with pre-run modifier:     | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:0 robot_suite.robot     5. Test cases "Autotest name 1" and "Autotest name 3" will be executed. Test case "Autotest name 2" will be skipped.     6. To execute tests from TestRail test run only with a certain status, for example "failed" and "blocked":     | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:0:failed:blocked robot_suite.robot     6. To execute stable tests from TestRail test run with run analysis depth = 5:     | robot --prerunmodifier TestRailPreRunModifier:testrail_server_name:tester_user_name:tester_user_password:20:http:5 robot_suite.robot # noqa: E951 Pre-run modifier initialization.          *Args:*\n             _server_ - name of TestRail server;\n             _user_ - name of TestRail user;\n             _password_ - password of <PASSWORD>;\n             _run_id_ - ID of the test run;\n             _protocol_ - connecting protocol to TestRail server: http or https;\n             _results_depth_ - analysis depth of run results;\n             _status_names_ - name of test statuses in TestRail. Gets list of 'testrailid' tags of the stable test cases.          Returns:             List of tags. Gets 'testrailid' tags.          Returns:             List of tags. Log message to the parent suite.          *Args:*\n             _suite_ - Robot Framework test suite object.             _message_ - message. Get list of 'testrailid' tags.          If required test statuses from the test run are passed to modifier,         a request is made to the TestRail to obtain information about all the statuses.         Theirs identifiers will be retrieved from list of all the statuses.         This identifiers will be used to receive test tags in the required status.          If statuses aren't passed to modifier,         the tags of all tests in the test run will be obtained regardless of their status.          Returns:             List of tags. Get list of 'testrailid' tags of the stable test cases.          If analysis depth of the run results is passed to modifier and its value greater than zero,         a request is made to the TestRail to receive information about test cases whose last result is 'passed'.         Based on the information received, the results of the latest runs for these test cases are analyzed,         on the basis of which the tags of stable test cases will be received.          Returns:             List of stable tags. Get result from future with try/except block and to list.             Args:                 future: future object. Form list of tests for the Robot Framework test suite that are included in the TestRail test run.          If analysis depth of the run results is greater than zero, when first suite is launched         a list of 'testrailid' tags of stable test cases is obtained.         After that the list of tags is written to the class attribute and for subsequent suites the obtaining is not happening.          If analysis depth of the run results is zero, when the first suite is launched         a list of 'testrailid' tags of all test cases in the given status is obtained.         After that the list of tags is written to the class attribute and for subsequent suites the obtaining is not happening.          *Args:*\n             _suite_ - Robot Framework test suite object. Removing test suites that are empty after excluding tests that are not part of the TestRail test run.          *Args:*\n             _suite_ - Robot Framework test suite object.