# Copyright 2014 Google Inc. All Rights Reserved.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Tests in OpenHTF.

Tests are main entry point for OpenHTF tests.  In its simplest form a
test is a series of Phases that are executed by the OpenHTF framework.

"""
import argparse
import collections
import logging
import os
import sys
import textwrap
import threading
from types import LambdaType
import uuid
import weakref

import colorama
import mutablerecords

from openhtf import util
from openhtf.core import phase_descriptor
from openhtf.core import phase_executor
from openhtf.core import phase_group
from openhtf.core import test_executor
from openhtf.core import test_record

from openhtf.util import conf
from openhtf.util import console_output
from openhtf.util import logs

import six

_LOG = logging.getLogger(__name__)

conf.declare('capture_source', description=textwrap.dedent(
    '''Whether to capture the source of phases and the test module.  This
    defaults to False since this potentially reads many files and makes large
    string copies.

    Set to 'true' if you want to capture your test's source.'''),
             default_value=False)
# TODO(arsharma): Deprecate this configuration after removing the old teardown
# specification.
conf.declare('teardown_timeout_s', default_value=30, description=
             'Default timeout (in seconds) for test teardown functions; '
             'this option is deprecated and only applies to the deprecated '
             'Test level teardown function.')


class UnrecognizedTestUidError(Exception):
  """Raised when information is requested about an unknown Test UID."""


class InvalidTestPhaseError(Exception):
  """Raised when an invalid method is decorated."""


class InvalidTestStateError(Exception):
  """Raised when an operation is attempted in an invalid state."""


def create_arg_parser(add_help=False):
  """Creates an argparse.ArgumentParser for parsing command line flags.

  If you want to add arguments, create your own with this as a parent:

  >>> parser = argparse.ArgumentParser(
          'My args title', parents=[openhtf.create_arg_parser()])
  >>> parser.parse_args()

  Args:
    add_help: boolean option passed through to arg parser.

  Returns:
    an `argparse.ArgumentParser`

  """
  parser = argparse.ArgumentParser(
      'OpenHTF-based testing',
      parents=[
          conf.ARG_PARSER,
          console_output.ARG_PARSER,
          logs.ARG_PARSER,
          phase_executor.ARG_PARSER,
      ],
      add_help=add_help)
  parser.add_argument(
      '--config-help', action='store_true',
      help='Instead of executing the test, simply print all available config '
      'keys and their description strings.')
  return parser


class Test(object):
  """An object that represents an OpenHTF test.

  Example:

    def PhaseOne(test):
      # Integrate more widgets

    def PhaseTwo(test):
      # Analyze widget integration status

    Test(PhaseOne, PhaseTwo).execute()

  Note that Test() objects *must* be created in the main thread, but can be
  .execute()'d in a separate thread.
  """

  TEST_INSTANCES = weakref.WeakValueDictionary()
  HANDLED_SIGINT_ONCE = False

  def __init__(self, *phases, **metadata):
    # Some sanity checks on special metadata keys we automatically fill in.
    if 'config' in metadata:
      raise KeyError(
          'Invalid metadata key "config", it will be automatically populated.')

    self.created_time_millis = util.time_millis()
    self.last_run_time_millis = None
    self._test_options = TestOptions()
    self._lock = threading.Lock()
    self._executor = None
    self._test_desc = TestDescriptor(
        phases, test_record.CodeInfo.uncaptured(), metadata)

    if conf.capture_source:
      # First, we copy the phases with the real CodeInfo for them.
      group = self._test_desc.phase_group.load_code_info()

      # Then we replace the TestDescriptor with one that stores the test
      # module's CodeInfo as well as our newly copied phases.
      code_info = test_record.CodeInfo.for_module_from_stack(levels_up=2)
      self._test_desc = self._test_desc._replace(
          code_info=code_info, phase_group=group)

    # Make sure configure() gets called at least once before Execute().  The
    # user might call configure() again to override options, but we don't want
    # to force them to if they want to use defaults.  For default values, see
    # the class definition of TestOptions.
    if 'test_name' in metadata:
      # Allow legacy metadata key for specifying test name.
      self.configure(name=metadata['test_name'])
    else:
      self.configure()

  @classmethod
  def from_uid(cls, test_uid):
    """Get Test by UID.

    Args:
      test_uid:  uuid for desired test.

    Returns:
      Test object for given by UID.

    Raises:
      UnrecognizedTestUidError: If the test_uid is not recognized.
    """
    test = cls.TEST_INSTANCES.get(test_uid)
    if not test:
      raise UnrecognizedTestUidError('Test UID %s not recognized' % test_uid)
    return test

  @property
  def uid(self):
    if self._executor is not None:
      return self._executor.uid

  def make_uid(self):
    """Returns the next test execution's UID.

    This identifier must be unique but trackable across invocations of
    execute(). Therefore, it's made of four parts separated by ':'
    * Process-specific (decided on process start up)
    * Test descriptor-specific (decided on descriptor creation)
    * Execution-specific (decided on test start)