"""Smoke tests to verify basic communication to all AWS services.

If you want to control what services/regions are used you can
also provide two separate env vars:

    * AWS_SMOKE_TEST_REGION - The region used to create clients.
    * AWS_SMOKE_TEST_SERVICES - A CSV list of service names to test.

Otherwise, the ``REGION`` variable specifies the default region
to use and all the services in SMOKE_TESTS/ERROR_TESTS will be tested.

"""
import os
import mock
from pprint import pformat
import warnings
import logging
from nose.tools import assert_equal, assert_true

from tests import ClientHTTPStubber
from botocore import xform_name
import botocore.session
from botocore.client import ClientError
from botocore.endpoint import Endpoint
from botocore.exceptions import ConnectionClosedError


# Mapping of service -> api calls to try.
# Each api call is a dict of OperationName->params.
# Empty params means that the operation will be called with no params.  This is
# used as a quick verification that we can successfully make calls to services.
SMOKE_TESTS = {
 'acm': {'ListCertificates': {}},
 'apigateway': {'GetRestApis': {}},
 'application-autoscaling': {
     'DescribeScalableTargets': {
         'ServiceNamespace': 'ecs'
     }},
 'autoscaling': {'DescribeAccountLimits': {},
                 'DescribeAdjustmentTypes': {}},
 'cloudformation': {'DescribeStacks': {},
                    'ListStacks': {}},
 'cloudfront': {'ListDistributions': {},
                'ListStreamingDistributions': {}},
 'cloudhsmv2': {'DescribeBackups': {}},
 'cloudsearch': {'DescribeDomains': {},
                 'ListDomainNames': {}},
 'cloudtrail': {'DescribeTrails': {}},
 'cloudwatch': {'ListMetrics': {}},
 'codecommit': {'ListRepositories': {}},
 'codedeploy': {'ListApplications': {}},
 'codepipeline': {'ListActionTypes': {}},
 'cognito-identity': {'ListIdentityPools': {'MaxResults': 1}},
 'cognito-sync': {'ListIdentityPoolUsage': {}},
 'config': {'DescribeDeliveryChannels': {}},
 'datapipeline': {'ListPipelines': {}},
 'devicefarm': {'ListProjects': {}},
 'directconnect': {'DescribeConnections': {}},
 'ds': {'DescribeDirectories': {}},
 'dynamodb': {'ListTables': {}},
 'dynamodbstreams': {'ListStreams': {}},
 'ec2': {'DescribeRegions': {},
         'DescribeInstances': {}},
  'ecr': {'DescribeRepositories': {}},
 'ecs': {'DescribeClusters': {}},
 'elasticache': {'DescribeCacheClusters': {}},
 'elasticbeanstalk': {'DescribeApplications': {}},
 'elastictranscoder': {'ListPipelines': {}},
 'elb': {'DescribeLoadBalancers': {}},
 'emr': {'ListClusters': {}},
 'es': {'ListDomainNames': {}},
 'events': {'ListRules': {}},
  'firehose': {'ListDeliveryStreams': {}},
 'gamelift': {'ListBuilds': {}},
 'glacier': {'ListVaults': {}},
 'iam': {'ListUsers': {}},
 # Does not work with session credentials so
 # importexport tests are not run.
 #'importexport': {'ListJobs': {}},
 'importexport': {},
 'inspector': {'DescribeCrossAccountAccessRole': {}},
 'iot': {'DescribeEndpoint': {}},
 'kinesis': {'ListStreams': {}},
 'kms': {'ListKeys': {}},
 'lambda': {'ListFunctions': {}},
 'logs': {'DescribeLogGroups': {}},
 'opsworks': {'DescribeStacks': {}},
 'rds': {'DescribeDBInstances': {}},
 'redshift': {'DescribeClusters': {}},
 'route53': {'ListHostedZones': {}},
 'route53domains': {'ListDomains': {}},
 's3': {'ListBuckets': {}},
 'sdb': {'ListDomains': {}},
 'ses': {'ListIdentities': {}},
 'shield': {'GetSubscriptionState': {}},
 'sns': {'ListTopics': {}},
 'sqs': {'ListQueues': {}},
 'ssm': {'ListDocuments': {}},
 'storagegateway': {'ListGateways': {}},
 # sts tests would normally go here, but
 # there aren't any calls you can make when
 # using session credentials so we don't run any
 # sts tests.
 'sts': {},
 #'sts': {'GetSessionToken': {}},
 # Subscription needed for support API calls.
 'support': {},
 'swf': {'ListDomains': {'registrationStatus': 'REGISTERED'}},
 'waf': {'ListWebACLs': {'Limit': 1}},
 'workspaces': {'DescribeWorkspaces': {}},
}


# Same thing as the SMOKE_TESTS hash above, except these verify
# that we get an error response back from the server because
# we've sent invalid params.
ERROR_TESTS = {
    'apigateway': {'GetRestApi': {'restApiId': 'fake-id'}},
    'application-autoscaling': {
        'DescribeScalableTargets': {
            'ServiceNamespace': 'fake-service-namespace'
        }},
    'autoscaling': {'CreateLaunchConfiguration': {
        'LaunchConfigurationName': 'foo',
        'ImageId': 'ami-12345678',
        'InstanceType': 'm1.small',
        }},
    'cloudformation': {'CreateStack': {
        'StackName': 'fake',
        'TemplateURL': 'http://s3.amazonaws.com/foo/bar',
        }},
    'cloudfront': {'GetDistribution': {'Id': 'fake-id'}},
    'cloudhsmv2': {'ListTags': {'ResourceId': 'fake-id'}},
    'cloudsearch': {'DescribeIndexFields': {'DomainName': 'fakedomain'}},
    'cloudtrail': {'DeleteTrail': {'Name': 'fake-trail'}},
    'cloudwatch': {'SetAlarmState': {
        'AlarmName': 'abc',
        'StateValue': 'mno',
        'StateReason': 'xyz',
        }},
    'logs': {'GetLogEvents': {'logGroupName': 'a', 'logStreamName': 'b'}},
    'codecommit': {'ListBranches': {'repositoryName': 'fake-repo'}},
    'codedeploy': {'GetDeployment': {'deploymentId': 'fake-id'}},
    'codepipeline': {'GetPipeline': {'name': 'fake-pipeline'}},
    'cognito-identity': {'DescribeIdentityPool': {'IdentityPoolId': 'fake'}},
    'cognito-sync': {'DescribeIdentityPoolUsage': {'IdentityPoolId': 'fake'}},
    'config': {
        'GetResourceConfigHistory': {'resourceType': '', 'resourceId': 'fake'},
        },
    'datapipeline': {'GetPipelineDefinition': {'pipelineId': 'fake'}},
    'devicefarm': {'GetDevice': {'arn': 'arn:aws:devicefarm:REGION::device:f'}},
    'directconnect': {'DescribeConnections': {'connectionId': 'fake'}},
    'ds': {'CreateDirectory': {'Name': 'n', 'Password': 'p', 'Size': '1'}},
    'dynamodb': {'DescribeTable': {'TableName': 'fake'}},
    'dynamodbstreams': {'DescribeStream': {'StreamArn': 'x'*37}},
    'ec2': {'DescribeInstances': {'InstanceIds': ['i-12345678']}},
    'ecs': {'StopTask': {'task': 'fake'}},
    'efs': {'DeleteFileSystem': {'FileSystemId': 'fake'}},
    'elasticache': {'DescribeCacheClusters': {'CacheClusterId': 'fake'}},
    'elasticbeanstalk': {
        'DescribeEnvironmentResources': {'EnvironmentId': 'x'},
        },
    'elb': {'DescribeLoadBalancers': {'LoadBalancerNames': ['fake']}},
    'elastictranscoder': {'ReadJob': {'Id': 'fake'}},
    'emr': {'DescribeCluster': {'ClusterId': 'fake'}},
    'es': {'DescribeElasticsearchDomain': {'DomainName': 'not-a-domain'}},
    'gamelift': {'DescribeBuild': {'BuildId': 'fake-build-id'}},
    'glacier': {'ListVaults': {'accountId': 'fake'}},
    'iam': {'GetUser': {'UserName': 'fake'}},
    'kinesis': {'DescribeStream': {'StreamName': 'fake'}},
    'kms': {'GetKeyPolicy': {'KeyId': 'fake', 'PolicyName': 'fake'}},
    'lambda': {'Invoke': {'FunctionName': 'fake'}},
    'opsworks': {'DescribeLayers': {'StackId': 'fake'}},
    'rds': {'DescribeDBInstances': {'DBInstanceIdentifier': 'fake'}},
    'redshift': {'DescribeClusters': {'ClusterIdentifier': 'fake'}},
    'route53': {'GetHostedZone': {'Id': 'fake'}},
    'route53domains': {'GetDomainDetail': {'DomainName': 'fake'}},
    's3': {'ListObjects': {'Bucket': 'thisbucketdoesnotexistasdf'}},
    'ses': {'VerifyEmailIdentity': {'EmailAddress': 'fake'}},
    'sdb': {'CreateDomain': {'DomainName': ''}},
    'sns': {
        'ConfirmSubscription': {'TopicArn': 'a', 'Token': 'b'},
        'Publish': {'Message': 'hello', 'TopicArn': 'fake'},
        },
    'sqs': {'GetQueueUrl': {'QueueName': 'fake'}},
    'ssm': {'GetDocument': {'Name': 'fake'}},
    'storagegateway': {'ListVolumes': {'GatewayARN': 'x'*50}},
    'sts': {'GetFederationToken': {'Name': 'fake', 'Policy': 'fake'}},
    'support': {'CreateCase': {
        'subject': 'x',
        'communicationBody': 'x',
        'categoryCode': 'x',
        'serviceCode': 'x',
        'severityCode': 'low',
        }},
    'swf': {'DescribeDomain': {'name': 'fake'}},
    'waf': {'GetWebACL': {'WebACLId': 'fake'}},
    'workspaces': {'DescribeWorkspaces': {'DirectoryId': 'fake-directory-id'}},
}

REGION = 'us-east-1'
REGION_OVERRIDES = {
    'devicefarm': 'us-west-2',
    'efs': 'us-west-2',
    'inspector': 'us-west-2',
}
MAX_RETRIES = 8
logger = logging.getLogger(__name__)


def _get_client(session, service):
    if os.environ.get('AWS_SMOKE_TEST_REGION', ''):
        region_name = os.environ['AWS_SMOKE_TEST_REGION']
    else:
        region_name = REGION_OVERRIDES.get(service, REGION)
    client = session.create_client(service, region_name=region_name)
    client.meta.events.register_first('needs-retry.*.*', retry_handler)
    return client


def retry_handler(response, attempts, **kwargs):
    if response is not None:
        _, parsed = response
        code = parsed.get('Error', {}).get('Code')
        # Catch ThrottleException, Throttling.
        is_throttle_error = code is not None and 'throttl' in code.lower()
        if is_throttle_error and attempts <= MAX_RETRIES:
            # We want the exponential behavior with a fixed 10 second
            # minimum, e.g. 11, 12, 14, 18, 26.  With a max retries of 8,
            # this is about 7-8 minutes total we'll retry.
            retry_delay = (2 ** (attempts - 1)) + 10
            logger.debug("Using custom retry delay of: %s", retry_delay)
            return retry_delay


def _list_services(dict_entries):
    # List all services in the provided dict_entry.
    # If the AWS_SMOKE_TEST_SERVICES is provided,
    # it's a comma separated list of services you can provide
    # if you only want to run the smoke tests for certain services.
    if 'AWS_SMOKE_TEST_SERVICES' not in os.environ:
        return dict_entries.keys()
    else:
        wanted_services = os.environ.get(
            'AWS_SMOKE_TEST_SERVICES', '').split(',')
        return [key for key in dict_entries if key in wanted_services]


def test_can_make_request_with_client():
    # Same as test_can_make_request, but with Client objects
    # instead of service/operations.
    session = botocore.session.get_session()
    for service_name in _list_services(SMOKE_TESTS):
        client = _get_client(session, service_name)
        for operation_name in SMOKE_TESTS[service_name]:
            kwargs = SMOKE_TESTS[service_name][operation_name]
            method_name = xform_name(operation_name)
            yield _make_client_call, client, method_name, kwargs


def _make_client_call(client, operation_name, kwargs):
    method = getattr(client, operation_name)
    with warnings.catch_warnings(record=True) as caught_warnings:
        response = method(**kwargs)
        assert_equal(len(caught_warnings), 0,
                     "Warnings were emitted during smoke test: %s"
                     % caught_warnings)
        assert_true('Errors' not in response)


def test_can_make_request_and_understand_errors_with_client():
    session = botocore.session.get_session()
    for service_name in _list_services(ERROR_TESTS):
        client = _get_client(session, service_name)
        for operation_name in ERROR_TESTS[service_name]:
            kwargs = ERROR_TESTS[service_name][operation_name]
            method_name = xform_name(operation_name)
            yield _make_error_client_call, client, method_name, kwargs


def _make_error_client_call(client, operation_name, kwargs):
    method = getattr(client, operation_name)
    try:
        response = method(**kwargs)
    except ClientError as e:
        pass
    else:
        raise AssertionError("Expected client error was not raised "
                             "for %s.%s" % (client, operation_name))


def test_client_can_retry_request_properly():
    session = botocore.session.get_session()
    for service_name in _list_services(SMOKE_TESTS):
        client = _get_client(session, service_name)
        for operation_name in SMOKE_TESTS[service_name]:
            kwargs = SMOKE_TESTS[service_name][operation_name]
            yield (_make_client_call_with_errors, client,
                   operation_name, kwargs)


def _make_client_call_with_errors(client, operation_name, kwargs):
    operation = getattr(client, xform_name(operation_name))
    exception = ConnectionClosedError(endpoint_url='https://mock.eror')
    with ClientHTTPStubber(client, strict=False) as http_stubber:
        http_stubber.responses.append(exception)
        try:
            response = operation(**kwargs)
        except ClientError as e:
            assert False, ('Request was not retried properly, '
                           'received error:\n%s' % pformat(e))
        # Ensure we used the stubber as we're not using it in strict mode
        assert len(http_stubber.responses) == 0, 'Stubber was not used!'
