# Copyright 2015 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.

"""Functions for making lists of tests, and an AJAX endpoint to list tests.

This module contains functions for listing:
 - Sub-tests for a given test suite (in a tree structure).
 - Tests which match a given test path pattern.
"""

import json

from google.appengine.ext import ndb

from dashboard import layered_cache
from dashboard.common import request_handler
from dashboard.common import utils
from dashboard.models import graph_data


class BadRequestError(Exception):
  pass


class ListTestsHandler(request_handler.RequestHandler):
  """URL endpoint for AJAX requests to list masters, bots, and tests."""

  def post(self):
    """Outputs a JSON string of the requested list.

    Request parameters:
      type: Type of list to make, one of "suite", "sub_tests" or "pattern".
      suite: Test suite name (applies only if type is "sub_tests").
      bots: Comma-separated bots name (applies only if type is "sub_tests").
      p: Test path pattern (applies only if type is "pattern").
      has_rows: "1" if the requester wants to list only list tests that
          have points (applies only if type is "pattern").
      test_path_dict: A test path dict having the format specified in
          GetTestsForTestPathDict, below (applies only if type is
          "test_path_dict").
      return_selected: "1" if the requester wants to return selected tests,
          otherwise unselected tests will be returned (applies only if type is
          "test_path_dict").

    Outputs:
      A data structure with test names in JSON format, or nothing.
    """
    self.response.headers.add_header('Access-Control-Allow-Origin', '*')
    list_type = self.request.get('type')
    # TODO(qyearsley): Separate these into two different handlers.

    if list_type == 'sub_tests':
      suite_name = self.request.get('suite')
      bot_names = self.request.get('bots').split(',')
      test_list = GetSubTests(suite_name, bot_names)
      self.response.out.write(json.dumps(test_list))

    if list_type == 'pattern':
      pattern = self.request.get('p')
      only_with_rows = self.request.get('has_rows') == '1'
      test_list = GetTestsMatchingPattern(
          pattern, only_with_rows=only_with_rows)
      self.response.out.write(json.dumps(test_list))

    if list_type == 'test_path_dict':
      test_path_dict = self.request.get('test_path_dict')
      return_selected = self.request.get('return_selected') == '1'
      try:
        test_list = GetTestsForTestPathDict(
            json.loads(test_path_dict), return_selected)
        self.response.out.write(json.dumps(test_list))
      except BadRequestError as e:
        self.ReportError(e.message, status=400)


def GetSubTests(suite_name, bot_names):
  """Gets the entire tree of subtests for the suite with the given name.

  Each bot may have different sub-tests available, but there is one combined
  sub-tests dict returned for all the bots specified.

  This method is used by the test-picker select menus to display what tests
  are available; only tests that are not deprecated should be listed.

  Args:
    suite_name: Top level test name.
    bot_names: List of master/bot names in the form "<master>/<platform>".

  Returns:
    A dict mapping test names to dicts to entries which have the keys
    "has_rows" (boolean) and "sub_tests", which is another sub-tests dict.
    This forms a tree structure.
  """
  # For some bots, there may be cached data; First collect and combine this.
  combined = {}
  for bot_name in bot_names:
    master, bot = bot_name.split('/')
    suite_key = ndb.Key('TestMetadata', '%s/%s/%s' % (master, bot, suite_name))

    cache_key = _ListSubTestCacheKey(suite_key)
    cached = layered_cache.Get(cache_key)
    if cached:
      combined = _MergeSubTestsDict(combined, json.loads(cached))
    else:
      sub_test_paths_futures = GetTestDescendantsAsync(
          suite_key, has_rows=True, deprecated=False)
      deprecated_sub_test_path_futures = GetTestDescendantsAsync(
          suite_key, has_rows=True, deprecated=True)

      ndb.Future.wait_all(
          [sub_test_paths_futures, deprecated_sub_test_path_futures])
      sub_test_paths = _MapTestDescendantsToSubTestPaths(
          sub_test_paths_futures.get_result())
      deprecated_sub_test_paths = _MapTestDescendantsToSubTestPaths(
          deprecated_sub_test_path_futures.get_result())

      d1 = _SubTestsDict(sub_test_paths, False)
      d2 = _SubTestsDict(deprecated_sub_test_paths, True)

      sub_tests = _MergeSubTestsDict(d1, d2)

      # Pickle is actually really slow, json.dumps to bypass that.
      layered_cache.Set(cache_key, json.dumps(sub_tests))

      combined = _MergeSubTestsDict(combined, sub_tests)

  return combined

def _SubTestsDict(paths, deprecated):
  """Constructs a sub-test dict from a list of test paths.

  Args:
    paths: An iterable of test paths for which there are points. Each test
        path is of the form "Master/bot/benchmark/chart/...". Each test path
        corresponds to a TestMetadata entity for which has_rows is set to True.
    deprecated: Whether test are deprecated.

  Returns:
    A recursively nested dict of sub-tests, as returned by GetSubTests.
  """
  merged = {}
  for p in paths:
    test_name = p[0]
    if not test_name in merged:
      merged[test_name] = {'has_rows': False, 'sub_tests': []}
    sub_test_path = p[1:]

    # If this is a top level name, then there are rows
    if not sub_test_path:
      merged[test_name]['has_rows'] = True
    else:
      merged[test_name]['sub_tests'].append(sub_test_path)

  if deprecated:
    for k, v in merged.iteritems():
      merged[k]['deprecated'] = True

  for k, v in merged.iteritems():
    merged[k]['sub_tests'] = _SubTestsDict(v['sub_tests'], deprecated)
  return merged


def _MapTestDescendantsToSubTestPaths(keys):
  """Makes a list of partial test paths for descendants of a test suite.

  Args:
    keys: A list of TestMetadata keys

  Returns:
    A list of test paths for all descendant TestMetadata entities that have
    associated Row entities. These test paths omit the Master/bot/suite part.
  """
  return map(_SubTestPath, keys)


def _SubTestPath(test_key):
  """Returns the part of a test path starting from after the test suite."""
  full_test_path = utils.TestPath(test_key)
  parts = full_test_path.split('/')
  assert len(parts) > 3
  return parts[3:]


def _ListSubTestCacheKey(test_key):
  """Returns the sub-tests list cache key for a test suite."""
  parts = utils.TestPath(test_key).split('/')
  master, bot, suite = parts[0:3]
  return graph_data.LIST_TESTS_SUBTEST_CACHE_KEY % (master, bot, suite)


def _MergeSubTestsDict(a, b):
  """Merges two sub-tests dicts together."""
  sub_tests = {}