# Copyright 2015 Amazon.com, Inc. or its affiliates. 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. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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.
import jmespath
import pytest
from jsonschema import Draft4Validator

import botocore.session
from botocore.exceptions import UnknownServiceError
from botocore.utils import ArgumentGenerator

WAITER_SCHEMA = {
    "type": "object",
    "properties": {
        "version": {"type": "number"},
        "waiters": {
            "type": "object",
            "additionalProperties": {
                "type": "object",
                "properties": {
                    "type": {"type": "string", "enum": ["api"]},
                    "operation": {"type": "string"},
                    "description": {"type": "string"},
                    "delay": {
                        "type": "number",
                        "minimum": 0,
                    },
                    "maxAttempts": {"type": "integer", "minimum": 1},
                    "acceptors": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "state": {
                                    "type": "string",
                                    "enum": ["success", "retry", "failure"],
                                },
                                "matcher": {
                                    "type": "string",
                                    "enum": [
                                        "path",
                                        "pathAll",
                                        "pathAny",
                                        "status",
                                        "error",
                                    ],
                                },
                                "argument": {"type": "string"},
                                "expected": {
                                    "oneOf": [
                                        {"type": "string"},
                                        {"type": "number"},
                                        {"type": "boolean"},
                                    ]
                                },
                            },
                            "required": ["state", "matcher", "expected"],
                            "additionalProperties": False,
                        },
                    },
                },
                "required": ["operation", "delay", "maxAttempts", "acceptors"],
                "additionalProperties": False,
            },
        },
    },
    "additionalProperties": False,
}


def _waiter_configs():
    session = botocore.session.get_session()
    validator = Draft4Validator(WAITER_SCHEMA)
    for service_name in session.get_available_services():
        client = session.create_client(service_name, 'us-east-1')
        try:
            # We use the loader directly here because we need the entire
            # json document, not just the portions exposed (either
            # internally or externally) by the WaiterModel class.
            loader = session.get_component('data_loader')
            waiter_model = loader.load_service_model(service_name, 'waiters-2')
        except UnknownServiceError:
            # The service doesn't have waiters
            continue
        yield validator, waiter_model, client


@pytest.mark.parametrize("validator, waiter_model, client", _waiter_configs())
def test_lint_waiter_configs(validator, waiter_model, client):
    _validate_schema(validator, waiter_model)
    for waiter_name in client.waiter_names:
        _lint_single_waiter(client, waiter_name, client.meta.service_model)


def _lint_single_waiter(client, waiter_name, service_model):
    try:
        waiter = client.get_waiter(waiter_name)
        # The 'acceptors' property is dynamic and will create
        # the acceptor configs when first accessed.  This is still
        # considered a failure to construct the waiter which is
        # why it's in this try/except block.
        # This catches things like:
        # * jmespath expression compiles
        # * matcher has a known value
        acceptors = waiter.config.acceptors
    except Exception as e:
        raise AssertionError(f"Could not create waiter '{waiter_name}': {e}")
    operation_name = waiter.config.operation
    # Needs to reference an existing operation name.
    if operation_name not in service_model.operation_names:
        raise AssertionError(
            f"Waiter config references unknown operation: {operation_name}"
        )
    # Needs to have at least one acceptor.
    if not waiter.config.acceptors:
        raise AssertionError(
            "Waiter config must have at least "
            f"one acceptor state: {waiter.name}"
        )
    op_model = service_model.operation_model(operation_name)
    for acceptor in acceptors:
        _validate_acceptor(acceptor, op_model, waiter.name)

    if not waiter.name.isalnum():
        raise AssertionError(f"Waiter name {waiter_name} is not alphanumeric.")


def _validate_schema(validator, waiter_json):
    errors = list(e.message for e in validator.iter_errors(waiter_json))
    if errors:
        raise AssertionError('\n'.join(errors))


def _validate_acceptor(acceptor, op_model, waiter_name):
    if acceptor.matcher.startswith('path'):
        expression = acceptor.argument
        # The JMESPath expression should have the potential to match something
        # in the response shape.
        output_shape = op_model.output_shape
        assert output_shape is not None, (
            f"Waiter '{waiter_name}' has JMESPath expression with no output shape: {op_model}"
        )
        # We want to check if the JMESPath expression makes sense.
        # To do this, we'll generate sample output and evaluate the
        # JMESPath expression against the output.  We'll then
        # check a few things about this returned search result.
        search_result = _search_jmespath_expression(expression, op_model)
        if search_result is None:
            raise AssertionError(
                f"JMESPath expression did not match anything for waiter "
                f"'{waiter_name}': {expression}"
            )
        if acceptor.matcher in ['pathAll', 'pathAny']:
            assert isinstance(search_result, list), (
                f"Attempted to use '{acceptor.matcher}' matcher in waiter "
                f"'{waiter_name}' with non list result in JMESPath expression: "
                f"{expression}"
            )


def _search_jmespath_expression(expression, op_model):
    arg_gen = ArgumentGenerator(use_member_names=True)
    sample_output = arg_gen.generate_skeleton(op_model.output_shape)
    search_result = jmespath.search(expression, sample_output)
    return search_result
