# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import re

# Note: this module is tested by a unit test config_validation_test.py,
# rather than recipe simulation tests.

_BISECT_CONFIG_SCHEMA = {
    'command': {'type': 'string', 'required': True},
    'good_revision': {'type': 'revision', 'required': True},
    'bad_revision': {'type': 'revision', 'required': True},
    'bisect_bot': {'type': 'string'},
    'metric': {'type': 'string'},
    'bug_id': {'type': 'integer'},
    'repeat_count': {'type': 'integer'},
    'max_time_minutes': {'type': 'integer'},
    'bisect_mode': {'type': 'string',
                    'choices': ['mean', 'return_code', 'std_dev']},
    'gs_bucket': {'type': 'string'},
    'builder_host': {'type': 'string'},
    'builder_port': {'type': 'integer'},
    'test_type': {'type': 'string'},
    'improvement_direction': {'type': 'integer'},
    'recipe_tester_name': {'type': 'string'},
    'try_job_id': {'type': 'integer'},
}


class ValidationFail(Exception):
  """An exception class that represents a failure to validate."""


def validate_bisect_config(config, schema=None):
  """Checks the correctness of the given bisect job config."""
  schema = _BISECT_CONFIG_SCHEMA if schema is None else schema
  for key in set(schema):
    validate_key(config, schema, key)

  if 'good_revision' in schema and 'bad_revision' in schema:
    _validate_revisions(config.get('good_revision'), config.get('bad_revision'))

  if 'bisect_mode' in schema and 'metric' in schema:
    _validate_metric(config.get('bisect_mode'), config.get('metric'))


def validate_key(config, schema, key):  # pragma: no cover
  """Checks the correctness of the given field in a config."""
  if schema[key].get('required') and config.get(key) is None:
    raise ValidationFail('Required key "%s" missing.' % key)
  if config.get(key) is None:
    return  # Optional field.
  value = config[key]
  field_type = schema[key].get('type')
  if field_type == 'string':
    _validate_string(value, key)
  elif field_type == 'integer':
    _validate_integer(value, key)
  elif field_type == 'revision':
    _validate_revision(value, key)
  elif field_type == 'boolean':
    _validate_boolean(value, key)
  if 'choices' in schema[key] and value not in schema[key]['choices']:
    _fail(value, key)


def _fail(value, key):
  raise ValidationFail('Invalid value %r for "%s".' % (value, key))


def _validate_string(value, key):  # pragma: no cover
  if not isinstance(value, basestring):
    _fail(value, key)


def _validate_revision(value, key):  # pragma: no cover
  s = str(value)
  if not (s.isdigit() or re.match('^[0-9A-Fa-f]{40}$', s)):
    



def _validate_integer(value, key):  # pragma: no cover
  try:
    int(value)
  except ValueError:
    _fail(value, key)


def _validate_boolean(value, key):  # pragma: no cover
  if value not in (True, False):
    _fail(value, key)


def _validate_revisions(good_revision, bad_revision):  # pragma: no cover
  try:
    earlier = int(good_revision)
    later = int(bad_revision)
  except ValueError:
    return  # The revisions could be sha1 hashes.
  if earlier >= later:
    raise ValidationFail('Order of good_revision (%d) and bad_revision(%d) '
                         'is reversed.' % (earlier, later))


def _validate_metric(bisect_mode, metric):  # pragma: no cover
  if bisect_mode not in ('mean', 'std_dev'):
    return
  if not (isinstance(metric, basestring) and metric.count('/') == 1):
    raise ValidationFail('Invalid value for "metric": %s' % metric)

_fail(value, key)