#!/usr/bin/env python3

# Copyright 2020 The gRPC Authors
#
# 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.

# Library to extract scenario definitions from scenario_config.py.
#
# Contains functions to filter, analyze and dump scenario definitions.
#
# This library is used in loadtest_config.py to generate the "scenariosJSON"
# field in the format accepted by the OSS benchmarks framework.
# See https://github.com/grpc/test-infra/blob/master/config/samples/cxx_example_loadtest.yaml
#
# It can also be used to dump scenarios to files, to count scenarios by
# language, and to export scenario languages in a format that can be used for
# automation.
#
# Example usage:
#
# scenario_config.py --export_scenarios -l cxx -f cxx_scenario_ -r '.*' \
#     --category=scalable
#
# scenario_config.py --count_scenarios
#
# scenario_config.py --count_scenarios --category=scalable
#
# For usage of the language config output, see loadtest_config.py.

import argparse
import collections
import json
import re
import sys
from typing import Any, Callable, Dict, Iterable, NamedTuple

import scenario_config

# Language parameters for load test config generation.

LanguageConfig = NamedTuple('LanguageConfig', [('category', str),
                                               ('language', str),
                                               ('client_language', str),
                                               ('server_language', str)])


def category_string(categories: Iterable[str], category: str) -> str:
    """Converts a list of categories into a single string for counting."""
    if category != 'all':
        return category if category in categories else ''

    main_categories = ('scalable', 'smoketest')
    s = set(categories)

    c = [m for m in main_categories if m in s]
    s.difference_update(main_categories)
    c.extend(s)
    return ' '.join(c)


def gen_scenario_languages(category: str) -> Iterable[LanguageConfig]:
    """Generates tuples containing the languages specified in each scenario."""
    for language in scenario_config.LANGUAGES:
        for scenario in scenario_config.LANGUAGES[language].scenarios():
            client_language = scenario.get('CLIENT_LANGUAGE', '')
            server_language = scenario.get('SERVER_LANGUAGE', '')
            categories = scenario.get('CATEGORIES', [])
            if category != 'all' and category not in categories:
                continue
            cat = category_string(categories, category)
            yield LanguageConfig(category=cat,
                                 language=language,
                                 client_language=client_language,
                                 server_language=server_language)


def scenario_filter(
    scenario_name_regex: str = '.*',
    category: str = 'all',
    client_language: str = '',
    server_language: str = '',
) -> Callable[[Dict[str, Any]], bool]:
    """Returns a function to filter scenarios to process."""

    def filter_scenario(scenario: Dict[str, Any]) -> bool:
        """Filters scenarios that match specified criteria."""
        if not re.search(scenario_name_regex, scenario["name"]):
            return False
        # if the 'CATEGORIES' key is missing, treat scenario as part of
        # 'scalable' and 'smoketest'. This matches the behavior of
        # run_performance_tests.py.
        scenario_categories = scenario.get('CATEGORIES',
                                           ['scalable', 'smoketest'])
        if category not in scenario_categories and category != 'all':
            return False

        scenario_client_language = scenario.get('CLIENT_LANGUAGE', '')
        if client_language != scenario_client_language:
            return False

        scenario_server_language = scenario.get('SERVER_LANGUAGE', '')
        if server_language != scenario_server_language:
            return False

        return True

    return filter_scenario


def gen_scenarios(
    language_name: str, scenario_filter_function: Callable[[Dict[str, Any]],
                                                           bool]
) -> Iterable[Dict[str, Any]]:
    """Generates scenarios that match a given filter function."""
    return map(
        scenario_config.remove_nonproto_fields,
        filter(scenario_filter_function,
               scenario_config.LANGUAGES[language_name].scenarios()))


def dump_to_json_files(scenarios: Iterable[Dict[str, Any]],
                       filename_prefix: str) -> None:
    """Dumps a list of scenarios to JSON files"""
    count = 0
    for scenario in scenarios:
        filename = '{}{}.json'.format(filename_prefix, scenario['name'])
        print('Writing file {}'.format(filename), file=sys.stderr)
        with open(filename, 'w') as outfile:
            # The dump file should have {"scenarios" : []} as the top level
            # element, when embedded in a LoadTest configuration YAML file.
            json.dump({'scenarios': [scenario]}, outfile, indent=2)
        count += 1
    print('Wrote {} scenarios'.format(count), file=sys.stderr)


def main() -> None:
    language_choices = sorted(scenario_config.LANGUAGES.keys())
    argp = argparse.ArgumentParser(description='Exports scenarios to files.')
    argp.add_argument('--export_scenarios',
                      action='store_true',
                      help='Export scenarios to JSON files.')
    argp.add_argument('--count_scenarios',
                      action='store_true',
                      help='Count scenarios for all test languages.')
    argp.add_argument('-l',
                      '--language',
                      choices=language_choices,
                      help='Language to export.')
    argp.add_argument('-f',
                      '--filename_prefix',
                      default='scenario_dump_',
                      type=str,
                      help='Prefix for exported JSON file names.')
    argp.add_argument('-r',
                      '--regex',
                      default='.*',
                      type=str,
                      help='Regex to select scenarios to run.')
    argp.add_argument(
        '--category',
        default='all',
        choices=['all', 'inproc', 'scalable', 'smoketest', 'sweep'],
        help='Select scenarios for a category of tests.')
    argp.add_argument(
        '--client_language',
        default='',
        choices=language_choices,
        help='Select only scenarios with a specified client language.')
    argp.add_argument(
        '--server_language',
        default='',
        choices=language_choices,
        help='Select only scenarios with a specified server language.')
    args = argp.parse_args()

    if args.export_scenarios and not args.language:
        print('Dumping scenarios requires a specified language.',
              file=sys.stderr)
        argp.print_usage(file=sys.stderr)
        return

    if args.export_scenarios:
        s_filter = scenario_filter(scenario_name_regex=args.regex,
                                   category=args.category,
                                   client_language=args.client_language,
                                   server_language=args.server_language)
        scenarios = gen_scenarios(args.language, s_filter)
        dump_to_json_files(scenarios, args.filename_prefix)

    if args.count_scenarios:
        print('Scenario count for all languages (category: {}):'.format(
            args.category))
        print('{:>5}  {:16} {:8} {:8} {}'.format('Count', 'Language', 'Client',
                                                 'Server', 'Categories'))
        c = collections.Counter(gen_scenario_languages(args.category))
        total = 0
        for ((cat, l, cl, sl), count) in c.most_common():
            print('{count:5}  {l:16} {cl:8} {sl:8} {cat}'.format(l=l,
                                                                 cl=cl,
                                                                 sl=sl,
                                                                 count=count,
                                                                 cat=cat))
            total += count

        print('\n{:>5}  total scenarios (category: {})'.format(
            total, args.category))


if __name__ == "__main__":
    main()
