# Copyright 2021 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.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
from typing import List
from unittest.mock import MagicMock

import pytest
from assertpy import assert_that

from pcluster.config.common import Resource, TypeMatchValidatorsSuppressor
from pcluster.validators.common import (
    AsyncValidator,
    FailureLevel,
    Validator,
    ValidatorContext,
    get_async_timed_validator_type_for,
)


class FakeInfoValidator(Validator):
    """Dummy validator of info level."""

    def _validate(self, param):
        self._add_failure(f"Wrong value {param}.", FailureLevel.INFO)


class FakeErrorValidator(Validator):
    """Dummy validator of error level."""

    def _validate(self, param):
        self._add_failure(f"Error {param}.", FailureLevel.ERROR)


class FakeAsyncInfoValidator(AsyncValidator):
    """Dummy validator of info level."""

    async def _validate_async(self, param):
        await asyncio.sleep(0.2)
        self._add_failure(f"Wrong async value {param}.", FailureLevel.INFO)


class FakeAsyncErrorValidator(AsyncValidator):
    """Dummy validator of error level."""

    async def _validate_async(self, param):
        await asyncio.sleep(0.5)
        self._add_failure(f"Error async {param}.", FailureLevel.ERROR)


class FakeAsyncMultiErrorValidator(AsyncValidator):
    """Dummy validator of error level."""

    async def _validate_async(self, param):
        await asyncio.sleep(0.5)
        self._add_failure(f"Error async 1 {param}.", FailureLevel.ERROR)
        self._add_failure(f"Error async 2 {param}.", FailureLevel.ERROR)


class FakeComplexValidator(Validator):
    """Dummy validator requiring multiple parameters as input."""

    def _validate(self, fake_attribute, other_attribute):
        self._add_failure(f"Combination {fake_attribute} - {other_attribute}.", FailureLevel.WARNING)


class FakePropertyValidator(Validator):
    """Dummy property validator of info level."""

    def _validate(self, property_value: str):
        self._add_failure(f"Wrong value {property_value}.", FailureLevel.INFO)


class FakeFaultyValidator(Validator):
    """Dummy validator that raises an unexpected error."""

    def _validate(self, param: str):
        raise RuntimeError("dummy fault")


def assert_validation_result(result, expected_level, expected_message):
    """Assert that validation results is the expected one, by checking level and message."""
    assert_that(result.level).is_equal_to(expected_level)
    assert_that(result.message).contains(expected_message)


def test_resource_validate():
    """Verify that validators are executed in the right order according to priorities with the expected params."""

    class FakeResource(Resource):
        """Fake resource class to test validators."""

        def __init__(self):
            super().__init__()
            self.fake_attribute = "fake-value"
            self.other_attribute = "other-value"

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(FakeErrorValidator, param=self.fake_attribute)
            self._register_validator(
                FakeComplexValidator,
                fake_attribute=self.fake_attribute,
                other_attribute=self.other_attribute,
            )
            self._register_validator(FakeInfoValidator, param=self.other_attribute)

    fake_resource = FakeResource()
    validation_failures = fake_resource.validate()

    # Verify high prio is the first of the list
    assert_validation_result(validation_failures[0], FailureLevel.ERROR, "Error fake-value.")
    assert_validation_result(validation_failures[1], FailureLevel.WARNING, "Combination fake-value - other-value.")
    assert_validation_result(validation_failures[2], FailureLevel.INFO, "Wrong value other-value.")


def test_resource_validate_unexpected_error():
    """Verify that an unexpected error thrown by a validator does not interrupt the validation process."""

    class FakeResource(Resource):
        """Fake resource class to test validators."""

        def __init__(self):
            super().__init__()
            self.fake_attribute = "fake-value"
            self.other_attribute = "other-value"

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(FakeErrorValidator, param=self.fake_attribute)
            self._register_validator(FakeFaultyValidator, param=self.fake_attribute)
            self._register_validator(FakeInfoValidator, param=self.other_attribute)

    fake_resource = FakeResource()
    validation_failures = fake_resource.validate()

    # Verify high prio is the first of the list
    assert_validation_result(validation_failures[0], FailureLevel.ERROR, "Error fake-value.")
    assert_validation_result(validation_failures[1], FailureLevel.ERROR, "dummy fault")
    assert_validation_result(validation_failures[2], FailureLevel.INFO, "Wrong value other-value.")


def test_async_resource_validation():
    """Verify that sync and async validators are executed in the right order according to priorities."""

    class FakeResource(Resource):
        """Fake resource class to test validators."""

        def __init__(self):
            super().__init__()
            self.fake_attribute = "fake-value"
            self.other_attribute = "other-value"

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(FakeErrorValidator, param=self.fake_attribute)
            self._register_validator(FakeInfoValidator, param=self.other_attribute)
            self._register_validator(FakeAsyncErrorValidator, param=self.fake_attribute)
            self._register_validator(FakeAsyncInfoValidator, param=self.other_attribute)

    fake_resource = FakeResource()
    validation_failures = fake_resource.validate()

    assert_validation_result(validation_failures[0], FailureLevel.ERROR, "Error fake-value.")
    assert_validation_result(validation_failures[1], FailureLevel.INFO, "Wrong value other-value.")
    assert_validation_result(validation_failures[2], FailureLevel.ERROR, "Error async fake-value.")
    assert_validation_result(validation_failures[3], FailureLevel.INFO, "Wrong async value other-value.")


def test_async_resource_validation_waited_at_top_level():
    """Verify that async validators are awaited once and suppressible."""

    class FakeResource(Resource):
        """Fake resource class to test validators."""

        def __init__(self, name):
            super().__init__()
            self.fake_attribute = f"fake-{name}"
            self.other_attribute = f"other-{name}"
            self._await_async_validators = MagicMock(wraps=self._await_async_validators)

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(FakeErrorValidator, param=self.fake_attribute)
            self._register_validator(FakeInfoValidator, param=self.other_attribute)
            self._register_validator(FakeAsyncMultiErrorValidator, param=self.fake_attribute)
            self._register_validator(FakeAsyncInfoValidator, param=self.other_attribute)

    fake_resource = FakeResource("root")
    fake_resource.nested1 = FakeResource("nested1")
    fake_resource.nested2 = FakeResource("nested2")
    validation_failures = fake_resource.validate(
        suppressors=[
            TypeMatchValidatorsSuppressor({"FakeInfoValidator"}),
            TypeMatchValidatorsSuppressor({"FakeAsyncInfoValidator"}),
        ]
    )

    fake_resource._await_async_validators.assert_called_once()
    fake_resource.nested1._await_async_validators.assert_not_called()
    fake_resource.nested2._await_async_validators.assert_not_called()

    assert_validation_result(validation_failures[0], FailureLevel.ERROR, "Error fake-nested1.")
    assert_validation_result(validation_failures[1], FailureLevel.ERROR, "Error fake-nested2.")
    assert_validation_result(validation_failures[2], FailureLevel.ERROR, "Error fake-root.")
    assert_validation_result(validation_failures[3], FailureLevel.ERROR, "Error async 1 fake-nested1.")
    assert_validation_result(validation_failures[4], FailureLevel.ERROR, "Error async 2 fake-nested1.")
    assert_validation_result(validation_failures[5], FailureLevel.ERROR, "Error async 1 fake-nested2.")
    assert_validation_result(validation_failures[6], FailureLevel.ERROR, "Error async 2 fake-nested2.")
    assert_validation_result(validation_failures[7], FailureLevel.ERROR, "Error async 1 fake-root.")
    assert_validation_result(validation_failures[8], FailureLevel.ERROR, "Error async 2 fake-root.")


def test_async_resource_validation_with_timeout():
    """Verify that async validators can fail due to timeout."""

    class FakeResource(Resource):
        """Fake resource class to test validators."""

        def __init__(self):
            super().__init__()
            self.fake_attribute = "fake-value"
            self.other_attribute = "other-value"

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(
                get_async_timed_validator_type_for(FakeAsyncErrorValidator), param=self.fake_attribute, timeout=0.1
            )
            self._register_validator(
                get_async_timed_validator_type_for(FakeAsyncInfoValidator), param=self.other_attribute, timeout=0
            )
            self._register_validator(
                get_async_timed_validator_type_for(FakeAsyncErrorValidator), param=self.fake_attribute, timeout=3
            )
            self._register_validator(
                get_async_timed_validator_type_for(FakeAsyncInfoValidator), param=self.other_attribute, timeout=4
            )

    fake_resource = FakeResource()
    validation_failures = fake_resource.validate()

    assert_validation_result(
        validation_failures[0],
        FailureLevel.WARNING,
        "Validation of ({'param': 'fake-value'}) timed out after 0.1 seconds.",
    )
    assert_validation_result(
        validation_failures[1],
        FailureLevel.WARNING,
        "Validation of ({'param': 'other-value'}) timed out after 0 seconds.",
    )
    assert_validation_result(validation_failures[2], FailureLevel.ERROR, "Error async fake-value.")
    assert_validation_result(validation_failures[3], FailureLevel.INFO, "Wrong async value other-value.")


def test_dynamic_property_validate():
    """Verify that validators of dynamic parameters are working as expected."""

    class FakeResource(Resource):
        """Fake resource class to test validators."""

        def __init__(self):
            super().__init__()
            self.deps_value = ""

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(FakePropertyValidator, property_value=self.dynamic_attribute)

        @property
        def dynamic_attribute(self):
            return f"dynamic-value: {self.deps_value}"

    fake_resource = FakeResource()
    validation_failures = fake_resource.validate()
    assert_validation_result(
        validation_failures[0], FailureLevel.INFO, f"Wrong value dynamic-value: {fake_resource.deps_value}."
    )

    fake_resource.deps_value = "test1"
    validation_failures = fake_resource.validate()
    assert_validation_result(
        validation_failures[0], FailureLevel.INFO, f"Wrong value dynamic-value: {fake_resource.deps_value}."
    )

    fake_resource.deps_value = "test2"
    validation_failures = fake_resource.validate()
    assert_validation_result(
        validation_failures[0], FailureLevel.INFO, f"Wrong value dynamic-value: {fake_resource.deps_value}."
    )


def test_nested_resource_validate():
    """Verify that validators of nested resources are executed correctly."""

    class FakeNestedResource(Resource):
        """Fake nested resource class to test validators."""

        def __init__(self, fake_value):
            super().__init__()
            self.fake_attribute = fake_value

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(FakeErrorValidator, param=self.fake_attribute)

    class FakeParentResource(Resource):
        """Fake resource class to test validators."""

        def __init__(self, nested_resource: FakeNestedResource, list_of_resources: List[FakeNestedResource]):
            super().__init__()
            self.fake_resource = nested_resource
            self.other_attribute = "other-value"
            self.list_of_resources = list_of_resources

        def _register_validators(self, context: ValidatorContext = None):
            self._register_validator(FakeInfoValidator, param=self.other_attribute)

    fake_resource = FakeParentResource(FakeNestedResource("value1"), [FakeNestedResource("value2")])
    validation_failures = fake_resource.validate()

    # Verify children failures are executed first
    assert_validation_result(validation_failures[0], FailureLevel.ERROR, "Error value1.")
    assert_validation_result(validation_failures[1], FailureLevel.ERROR, "Error value2.")
    assert_validation_result(validation_failures[2], FailureLevel.INFO, "Wrong value other-value.")


@pytest.mark.parametrize(
    "value, default, expected_value, expected_implied",
    [
        ("abc", "default_value", "abc", False),
        (None, "default_value", "default_value", True),
        (5, 10, 5, False),
        (None, 10, 10, True),
    ],
)
def test_resource_params(value, default, expected_value, expected_implied):
    class TestBaseBaseResource(Resource):
        def __init__(self):
            super().__init__()

    class TestBaseResource(TestBaseBaseResource):
        def __init__(self):
            super().__init__()

    class TestResource(TestBaseResource):
        def __init__(self):
            super().__init__()
            self.test_attr = Resource.init_param(value=value, default=default)

    test_resource = TestResource()
    assert_that(test_resource.test_attr).is_equal_to(expected_value)
    assert_that(test_resource.is_implied("test_attr")).is_equal_to(expected_implied)

    param = test_resource.get_param("test_attr")
    assert_that(param).is_not_none()
    assert_that(param.value).is_equal_to(expected_value)
    assert_that(param.default).is_equal_to(default)

    test_resource.test_attr = "new_value"
    assert_that(test_resource.is_implied("test_attr")).is_false()

    param = test_resource.get_param("test_attr")
    assert_that(param).is_not_none()
    assert_that(param.value).is_equal_to("new_value")
    assert_that(param.default).is_equal_to(default)
