#!/usr/bin/env python
#
# Copyright (c) 2014 Piston Cloud Computing, 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.
#


"""
Run Tempest and upload results to Refstack.

This module runs the Tempest test suite on an OpenStack environment given a
Tempest configuration file.

"""

import argparse
import ConfigParser
import json
import logging
import os
import subprocess
import time

from keystoneclient.v2_0 import client as ksclient

from subunit_processor import SubunitProcessor


class RefstackClient:
    log_format = "%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s"

    def __init__(self, args):
        '''Prepare a tempest test against a cloud.'''
        self.logger = logging.getLogger("refstack_client")
        self.console_log_handle = logging.StreamHandler()
        self.console_log_handle.setFormatter(
            logging.Formatter(self.log_format))
        self.logger.addHandler(self.console_log_handle)

        self.args = args

        if self.args.verbose > 1:
            self.logger.setLevel(logging.DEBUG)
        elif self.args.verbose == 1:
            self.logger.setLevel(logging.INFO)
        else:
            self.logger.setLevel(logging.ERROR)

    def _prep_test(self):
        '''Prepare a tempest test against a cloud.'''

        # Check that the config file exists.
        if not os.path.isfile(self.args.conf_file):
            self.logger.error("Conf file not valid: %s" % self.args.conf_file)
            exit(1)

        # Check that the Tempest directory is an existing directory.
        if not os.path.isdir(self.args.tempest_dir):
            self.logger.error("Tempest directory given is not a directory or "
                              "does not exist: %s" % self.args.tempest_dir)
            exit(1)

        self.tempest_script = os.path.join(self.args.tempest_dir,
                                           'run_tempest.sh')

        self.conf_file = self.args.conf_file
        self.conf = ConfigParser.SafeConfigParser()
        self.conf.read(self.args.conf_file)
        self.tempest_script = os.path.join(self.args.tempest_dir,
                                           'run_tempest.sh')

    def _prep_upload(self):
        '''Prepare an upload to the Refstack_api'''
        if not os.path.isfile(self.args.file):
            self.logger.error("File not valid: %s" % self.args.file)
            exit(1)
        self.logger.setLevel(logging.DEBUG)
        self.upload_file = self.args.file

    def _get_next_stream_subunit_output_file(self, tempest_dir):
        '''This method reads from the next-stream file in the .testrepository
           directory of the given Tempest path. The integer here is the name
           of the file where subunit output will be saved to.'''
        try:
            subunit_file = open(os.path.join(
                                tempest_dir, '.testrepository',
                                'next-stream'), 'r').read().rstrip()
        except (IOError, OSError):
            self.logger.debug('The .testrepository/next-stream file was not '
                              'found. Assuming subunit results will be stored '
                              'in file 0.')

            # Testr saves the first test stream to .testrepository/0 when
            # there is a newly generated .testrepository directory.
            subunit_file = "0"

        return os.path.join(tempest_dir, '.testrepository', subunit_file)

    def _get_cpid_from_keystone(self, conf_file):
        '''This will get the Keystone service ID which is used as the CPID.'''
        try:
            args = {'auth_url': conf_file.get('identity', 'uri'),
                    'username': conf_file.get('identity', 'admin_username'),
                    'password': conf_file.get('identity', 'admin_password')}

            if self.conf.has_option('identity', 'admin_tenant_id'):
                args['tenant_id'] = conf_file.get('identity',
                                                  'admin_tenant_id')
            else:
                args['tenant_name'] = conf_file.get('identity',
                                                    'admin_tenant_name')

            client = ksclient.Client(**args)
            services = client.services.list()
            for service in services:
                if service.type == "identity":
                    return service.id

        except ConfigParser.Error as e:
            # Most likely a missing section or option in the config file.
            self.logger.error("Invalid Config File: %s" % e)
            exit(1)

    def _form_result_content(self, cpid, duration, results):
        '''This method will create the content for the request. The spec at
           github.com/stackforge/refstack/blob/master/specs/approved/api-v1.md.
           defines the format expected by the API.'''
        content = {}
        content['cpid'] = cpid
        content['duration_seconds'] = duration
        content['results'] = results
        return content

    def _save_json_results(self, results, path):
        '''Save the output results from the Tempest run as a JSON file'''
        file = open(path, "w+")
        file.write(json.dumps(results, indent=4, separators=(',', ': ')))
        file.close()

    def get_passed_tests(self, result_file):
        '''Get a list of tests IDs that passed Tempest from a subunit file.'''
        subunit_processor = SubunitProcessor(result_file)
        results = subunit_processor.process_stream()
        return results

    def post_results(self, url, content):
        '''Post the combined results back to the server.'''

        # TODO(cdiep): Post results once the API is available as outlined here:
        # github.com/stackforge/refstack/blob/master/specs/approved/api-v1.md
        json_content = json.dumps(content)
        self.logger.debug('API request content: %s ' % json_content)

    def test(self):
        '''Execute Tempest test against the cloud.'''
        self._prep_test()
        results_file = self._get_next_stream_subunit_output_file(
            self.args.tempest_dir)
        cpid = self._get_cpid_from_keystone(self.conf)

        self.logger.info("Starting Tempest test...")
        start_time = time.time()

        # Run the tempest script, specifying the conf file, the flag
        # telling it to not use a virtual environment (-N), and the flag
        # telling it to run the tests serially (-t).
        cmd = (self.tempest_script, '-C', self.conf_file, '-N', '-t')

        # Add the tempest test cases to test as arguments. If no test
        # cases are specified, then all Tempest API tests will be run.
        if self.args.test_cases:
            cmd += ('--', self.args.test_cases)
        else:
            cmd += ('--', "tempest.api")

        # If there were two verbose flags, show tempest results.
        if self.args.verbose > 1:
            stderr = None
        else:
            # Suppress tempest results output. Note that testr prints
            # results to stderr.
            stderr = open(os.devnull, 'w')

        # Execute the tempest test script in a subprocess.
        process = subprocess.Popen(cmd, stderr=stderr)
        process.communicate()

        if process.returncode == 0:
            end_time = time.time()
            elapsed = end_time - start_time
            duration = int(elapsed)

            self.logger.info('Tempest test complete.')
            self.logger.info('Subunit results located in: %s' % results_file)

            results = self.get_passed_tests(results_file)
            self.logger.info("Number of passed tests: %d" % len(results))

            content = self._form_result_content(cpid, duration, results)
            json_path = results_file + ".json"
            self._save_json_results(content, json_path)
            self.logger.info('JSON results saved in: %s' % json_path)

            # If the user did not specify the offline argument, then upload
            # the results.
            if not self.args.offline:
                content = self._form_result_content(cpid, duration, results)
                self.post_results(self.args.url, content)
        else:
            self.logger.error("Problem executing Tempest script. Exit code %d",
                              process.returncode)

    def upload(self):
        '''Perform upload to Refstack URL.'''
        self._prep_upload()
        json_file = open(self.upload_file)
        json_data = json.load(json_file)
        json_file.close()
        self.post_results(self.args.url, json_data)


def parse_cli_args(args=None):

    usage_string = ('refstack-client [-h] {upload,test} ...\n\n'
                    'To see help on specific argument, do:\n'
                    'refstack-client <ARG> -h')

    parser = argparse.ArgumentParser(description='Refstack-client arguments',
                                     formatter_class=argparse.
                                     ArgumentDefaultsHelpFormatter,
                                     usage=usage_string)

    subparsers = parser.add_subparsers(help='Available subcommands.')

    # Arguments that go with all subcommands.
    shared_args = argparse.ArgumentParser(add_help=False)
    shared_args.add_argument('-v', '--verbose',
                             action='count',
                             help='Show verbose output.')

    shared_args.add_argument('--url',
                             action='store',
                             required=False,
                             default='https://api.refstack.org',
                             type=str,
                             help='Refstack API URL to upload results to '
                                  '(--url https://127.0.0.1:8000).')

    # Upload command
    parser_upload = subparsers.add_parser('upload', parents=[shared_args],
                                          help='Upload an existing result '
                                               'file. ')
    parser_upload.add_argument('file',
                               type=str,
                               help='Path of JSON results file.')
    parser_upload.set_defaults(func="upload")

    # Test command
    parser_test = subparsers.add_parser('test', parents=[shared_args],
                                        help='Run Tempest against a cloud.')

    parser_test.add_argument('--tempest-dir',
                             action='store',
                             required=False,
                             dest='tempest_dir',
                             default='.venv/src/tempest',
                             help='Path of the Tempest project directory.')

    parser_test.add_argument('-c', '--conf-file',
                             action='store',
                             required=True,
                             dest='conf_file',
                             type=str,
                             help='Path of the Tempest configuration file to '
                                  'use.')

    parser_test.add_argument('-t', '--test-cases',
                             action='store',
                             required=False,
                             dest='test_cases',
                             type=str,
                             help='Specify a subset of test cases to run '
                                  '(e.g. --test-cases tempest.api.compute).')

    parser_test.add_argument('--offline',
                             action='store_true',
                             help='Do not upload test results after running '
                                  'Tempest.')
    parser_test.set_defaults(func="test")

    return parser.parse_args(args=args)