# coding=utf-8
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
                        unicode_literals, with_statement)

import os
import re
import xml.etree.ElementTree as ET
from abc import abstractmethod
from threading import Timer

from pants.base.exceptions import ErrorWhileTesting
from pants.util.process_handler import subprocess


class TestRunnerTaskMixin(object):
  """A mixin to combine with test runner tasks.

  The intent is to migrate logic over time out of JUnitRun and PytestRun, so the functionality
  expressed can support both languages, and any additional languages that are added to pants.
  """

  @classmethod
  def register_options(cls, register):
    super(TestRunnerTaskMixin, cls).register_options(register)
    register('--skip', type=bool, help='Skip running tests.')
    register('--timeouts', type=bool, default=True,
             help='Enable test target timeouts. If timeouts are enabled then tests with a '
                  'timeout= parameter set on their target will time out after the given number of '
                  'seconds if not completed. If no timeout is set, then either the default timeout '
                  'is used or no timeout is configured. In the current implementation, all the '
                  'timeouts for the test targets to be run are summed and all tests are run with '
                  'the total timeout covering the entire run of tests. If a single target in a '
                  'test run has no timeout and there is no default, the entire run will have no '
                  'timeout. This should change in the future to provide more granularity.')
    register('--timeout-default', type=int, advanced=True,
             help='The default timeout (in seconds) for a test if timeout is not set on the '
                  'target.')
    register('--timeout-maximum', type=int, advanced=True,
             help='The maximum timeout (in seconds) that can be set on a test target.')
    register('--timeout-terminate-wait', type=int, advanced=True, default=10,
             help='If a test does not terminate on a SIGTERM, how long to wait (in seconds) before '
                  'sending a SIGKILL.')

  def execute(self):
    """Run the task."""

    # Ensure that the timeout_maximum is higher than the timeout default.
    if (self.get_options().timeout_maximum is not None
        and self.get_options().timeout_default is not None
        and self.get_options().timeout_maximum < self.get_options().timeout_default):
      message = "Error: timeout-default: {} exceeds timeout-maximum: {}".format(
        self.get_options().timeout_maximum,
        self.get_options().timeout_default
      )
      self.context.log.error(message)
      raise ErrorWhileTesting(message)

    if not self.get_options().skip:
      test_targets = self._get_test_targets()
      for target in test_targets:
        self._validate_target(target)

      all_targets = self._get_targets()
      self._execute(all_targets)

  def report_all_info_for_single_test(self, scope, target, test_name, test_info):
    """Add all of the test information for a single test.

    Given the dict of test information
    {'time': 0.124, 'result_code': 'success', 'classname': 'some.test.class'}
    iterate through each item and report the single item with _report_test_info.

    :param string scope: The scope for which we are reporting the information.
    :param Target target: The target that we want to store the test information under.
    :param string test_name: The test's name.
    :param dict test_info: The test's information, including run duration and result.
    """
    for test_info_key, test_info_val in test_info.items():
      key_list = [test_name, test_info_key]
      self._report_test_info(scope, target, key_list, test_info_val)

  def _report_test_info(self, scope, target, keys, test_info):
    """Add test information to target information.

    :param string scope: The scope for which we are reporting information.
    :param Target target: The target that we want to store the test information under.
    :param list of string keys: The keys that will point to the information being stored.
    :param primitive test_info: The information being stored.
    """
    if target and scope:
      target_type = target.type_alias
      self.context.run_tracker.report_target_info('GLOBAL', target, ['target_type'], target_type)
      self.context.run_tracker.report_target_info(scope, target, keys, test_info)

  @staticmethod
  def parse_test_info(xml_path, error_handler, additional_testcase_attributes=None):
    """Parses the junit file for information needed about each test.

    Will include:
      - test name
      - test result
      - test run time duration or None if not a parsable float

    If additional test case attributes are defined, then it will include those as well.

    :param string xml_path: The path of the xml file to be parsed.
    :param function error_handler: The error handler function.
    :param list of string additional_testcase_attributes: A list of additional attributes belonging
           to each testcase that should be included in test information.
    :return: A dictionary of test information.
    """
    tests_in_path = {}
    testcase_attributes = additional_testcase_attributes or []

    SUCCESS = 'success'
    SKIPPED = 'skipped'
    FAILURE = 'failure'
    ERROR = 'error'

    _XML_MATCHER = re.compile(r'^TEST-.+\.xml$')

    class ParseError(Exception):
      """Indicates an error parsing a xml report file."""

      def __init__(self, xml_path, cause):
        super(ParseError, self).__init__('Error parsing test result file {}: {}'
          .format(xml_path, cause))
        self.xml_path = xml_path
        self.cause = cause

    def parse_xml_file(xml_file_path):
      try:
        root = ET.parse(xml_file_path).getroot()
        for testcase in root.iter('testcase'):
          test_info = {}

          try:
            test_info.update({'time': float(testcase.attrib.get('time'))})
          except (TypeError, ValueError):
            test_info.update({'time': None})

          for attribute in testcase_attributes:
            test_info[attribute] = testcase.attrib.get(attribute)

          result = SUCCESS
          if next(testcase.iter('error'), None) is not None:
            result = ERROR
          elif next(testcase.iter('failure'), None) is not None:
            result = FAILURE
          elif next(testcase.iter('skipped'), None) is not None: