#    Copyright (c) 2016 Mirantis, Inc.
#
#    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.

from copy import deepcopy
import mock

from muranopkgcheck.tests import test_validator_helpers as helpers
from muranopkgcheck.validators import muranopl

MURANOPL_BASE = {
    'Name': 'Instance',
    'Namespaces': {
        '=': 'org.openstack.test',
        'res': 'io.murano.resources',
        'std': 'io.murano'},
    'Extends': 'res:LinuxMuranoInstance',
    'Properties': {
        'ports': {
            'Contract': '$.class(NeutronPort).notNull()',
            'Default': []}},
    'Methods': {
        'foo': {
            'Scope': 'Public',
            'Arguments': [{
                'arg1': {'Contract': '$.string()',
                         'Usage': 'Standard'}}],
            'Body': [
                {'Do': [
                    '$port.deploy()',
                    {'$template': {
                        'resources': {
                            '$.name': {
                                'properties': {
                                    'networks': [
                                        {'port': '$port.getRef()'}]}}}}},
                    {'$arg1':
                        '$arg1.mergeWith($template)'}],
                 'For': 'port',
                 'In': '$.ports'},
                {'$sth': 'new(res:Neutron)'},
                {'Return': '$arg1'}]}},
}


class MuranoPlTests(helpers.BaseValidatorTestClass):
    def setUp(self):
        super(MuranoPlTests, self).setUp()
        self.loaded_package = mock.Mock()
        self.loaded_package.format_version = '1.4'
        self.mpl_validator = muranopl.MuranoPLValidator(self.loaded_package)

    def test_import(self):
        self.g = self.mpl_validator._valid_import(['aaa.bbb', 'ccc.ddd',
                                                   'fff', 'w_ww'])

    def test_import_error(self):
        self.g = self.mpl_validator._valid_import(['aaa.bbb', 'ccc.ddd',
                                                   'fff', 'w_ww#'])
        self.assertIn('Wrong namespace or FNQ of extended class "w_ww#"',
                      next(self.g).message)

    def test_double_underscored_name(self):
        self.g = self.mpl_validator._valid_name('__Instance')
        self.assertIn('Invalid class name "__Instance"', next(self.g).message)

    def test_not_camel_case_name(self):
        self.g = self.mpl_validator._valid_name('notcamelcase')
        self.assertIn('Invalid class name "notcamelcase"',
                      next(self.g).message)

    def test_whitespace_in_name(self):
        name = 'white space'
        self.g = self.mpl_validator._valid_name(name)
        self.assertIn('Invalid class name "white space"',
                      next(self.g).message)

    def test_properties_list(self):
        self.g = self.mpl_validator._valid_properties([])
        self.assertIn('Properties should be a dict',
                      next(self.g).message)

    def test_properties_usage(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Usage'] = 'OutIn'
        self.g = self.mpl_validator._valid_properties(p_dict)
        self.assertIn('Not allowed usage "OutIn"',
                      next(self.g).message)

    def test_wrong_type_namespace(self):
        self.g = self.mpl_validator._valid_namespaces([1, 2, 3])
        self.assertIn('Wrong type of namespace',
                      next(self.g).message)

    def test_namespace_with_double_equal(self):
        self.g = self.mpl_validator._valid_namespaces({'==': 'std.io'})
        self.assertIn('Wrong name for namespace',
                      next(self.g).message)

    def test_namespace_with_wrong_fqn(self):
        self.g = self.mpl_validator._valid_namespaces({'=': 'io.murano@'})
        self.assertIn('Wrong namespace fqn "io.murano@"',
                      next(self.g).message)

    def test_wrong_method_scope(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Scope'] = 'Wrong'
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Wrong Scope "Wrong"',
                      next(self.g).message)

    def test_dict_in_body(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Body'] = {'$a': 'b'}
        self.g = self.mpl_validator._valid_methods(m_dict)

    def test_error_in_method_scalar_body(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Body'] = '$.deploy('
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('"$.deploy(" is not valid yaql expression',
                      next(self.g).message)

    def test_method_body_is_return(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Body'] = {'Return': '3'}
        self.g = self.mpl_validator._valid_methods(m_dict)

    def test_error_in_method_for_loop_in(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Body'][0]['In'] =\
            '$.deploy('
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('"$.deploy(" is not valid yaql expression',
                      next(self.g).message)

    def test_error_in_method_for_loop_body(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Body'][0]['Do'][1] =\
            '$.deploy('
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('"$.deploy(" is not valid yaql expression',
                      next(self.g).message)

    def test_missing_contract_in_properties(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        del p_dict['ports']['Contract']
        self.g = self.mpl_validator._valid_properties(p_dict)
        self.assertIn('Missing Contract in property "ports"',
                      next(self.g).message)

    def test_contract_is_not_yaql(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = '$.deploy('
        self.g = self.mpl_validator._valid_properties(p_dict)
        self.assertIn('Contract is not valid yaql "$.deploy("',
                      next(self.g).message)

    def test_contract_is_yaql_without_dollar(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = 'deploy()'
        self.g = self.mpl_validator._valid_properties(p_dict)
        self.assertIn('Contract is not valid yaql "deploy()"',
                      next(self.g).message)

    def test_contract_is_yaql_without_dot(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = '$deploy()'
        self.g = self.mpl_validator._valid_properties(p_dict)
        self.assertIn('Contract is not valid yaql "$deploy()"',
                      next(self.g).message)

    def test_contract_is_a_dict(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = {
            '$.int()': '$.string()',
            '$.string()': ['$.ports()']
        }
        self.g = self.mpl_validator._valid_properties(p_dict)

    def test_contract_is_a_dict_with_two_levels(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = {
            '$.int()': '$.string()',
            '$.string()': {'$.int()': '$.ports()', '$.int()': {
                '$': [], '$.int()': '$.string()'}}
        }
        self.g = self.mpl_validator._valid_properties(p_dict)

    def test_contract_is_a_dict_with_list(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = {
            '$.int()': [],
            '$.int()': {}
        }
        self.g = self.mpl_validator._valid_properties(p_dict)

    def test_contract_list_with_numbers(self):
        self.g = self.mpl_validator._valid_contract(['$.string()', 1, 3])

    def test_contract_is_a_list_with_dict(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = [{
            '$.string()': '$.string()',
            '$.int()': [{'$.int()': '$.string()', '$.int()': []}]}]
        self.g = self.mpl_validator._valid_properties(p_dict)

    def test_contract_is_a_list_two_elements(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = ['$.string()', '$.int()']
        self.g = self.mpl_validator._valid_properties(p_dict)

    def test_contract_is_a_number(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = 1
        self.g = self.mpl_validator._valid_properties(p_dict)
        self.assertIn('Contract is not valid yaql "1"',
                      next(self.g).message)

    def test_contract_list_with_min_length(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = ['$.int()', 1]
        self.g = self.mpl_validator._valid_properties(p_dict)

    def test_contract_a_list_with_invalid_yaql(self):
        p_dict = deepcopy(MURANOPL_BASE['Properties'])
        p_dict['ports']['Contract'] = ['$.string(']
        self.g = self.mpl_validator._valid_properties(p_dict)
        self.assertIn('Contract is not valid yaql "$.string("',
                      next(self.g).message)

    def test_extends_is_not_a_valid_list(self):
        p_dict = deepcopy(MURANOPL_BASE['Extends'])
        p_dict = ['abc:def', 1]
        self.g = self.mpl_validator._valid_extends(p_dict)
        self.assertIn('Wrong type of Extends field',
                      next(self.g).message)

    def test_extends_fqn_wrong(self):
        p_dict = deepcopy(MURANOPL_BASE['Extends'])
        p_dict = ['abc:def.afdsa.das#']
        self.g = self.mpl_validator._valid_extends(p_dict)
        self.assertIn('Wrong FNQ of extended class "abc:def.afdsa.das#"',
                      next(self.g).message)

    def test_extends_is_not_valid(self):
        p_dict = deepcopy(MURANOPL_BASE['Extends'])
        p_dict = 4
        self.g = self.mpl_validator._valid_extends(p_dict)
        self.assertIn('Wrong type of Extends field',
                      next(self.g).message)

    def test_method_invalid_name(self):
        m_dict = {'foo#': {}}
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Invalid name of method "foo#"',
                      next(self.g).message)

    def test_method_unknown_keyword(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        del m_dict['foo']['Body']
        m_dict['foo']['Body2'] = []
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Unknown keyword "Body2"',
                      next(self.g).message)

    def test_body_number(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Body'] = 1
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Body is not a list or scalar/yaql expression',
                      next(self.g).message)

    def test_dot_function(self):
        m_dict = {'.init': {}}
        self.g = self.mpl_validator._valid_methods(m_dict)

    def test_method_is_a_list(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo'] = [1, 2]
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Method is not a dict',
                      next(self.g).message)

    def test_method_scope_in_1_2(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        del m_dict['foo']['Arguments'][0]['arg1']['Usage']
        self.loaded_package.format_version = '1.2'
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Scope is not supported version earlier than 1.3',
                      next(self.g).message)

    def test_method_usage_action_in_1_4(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Usage'] = 'Action'
        self.loaded_package.format_version = '1.4'
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Usage "Action" is deprecated since 1.4',
                      next(self.g).message)

    def test_method_usage_action(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Usage'] = 'Action'
        self.loaded_package.format_version = '1.4'
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Usage "Action" is deprecated since 1.4',
                      next(self.g).message)

    def test_method_wrong_usage_action(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        m_dict['foo']['Usage'] = 'Runtimed'
        self.loaded_package.format_version = '1.4'
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Unsupported usage type "Runtimed"',
                      next(self.g).message)

    def test_method_wrong_usage_static_in_1_3(self):
        m_dict = deepcopy(MURANOPL_BASE['Methods'])
        del m_dict['foo']['Scope']
        del m_dict['foo']['Arguments'][0]['arg1']['Usage']
        m_dict['foo']['Usage'] = 'Static'
        self.loaded_package.format_version = '1.3'
        self.g = self.mpl_validator._valid_methods(m_dict)
        self.assertIn('Usage "Static" is available from 1.3',
                      next(self.g).message)

    def test_method_arguments_invalid(self):
        self.g = self.mpl_validator._valid_arguments('foobar')
        self.assertIn('Methods arguments should be a list or dict',
                      next(self.g).message)

    def test_method_arguments_are_dict(self):
        self.g = self.mpl_validator._valid_arguments({'a': {'Contract': '$'}})

    def test_method_arguments_are_dict_two_keys(self):
        self.g = self.mpl_validator._valid_arguments({'a': 'b', 'c': 'd'})
        self.assertIn('It is not safe to define methods arguments as a dict '
                      'with several keys', next(self.g).message)

    def test_method_arguments_element_is_two_key(self):
        arguments = [
            {'a': {'Contract': '$.string()'},
             'b': {'Contract': '$.string()'}}]
        self.g = self.mpl_validator._valid_arguments(arguments)
        self.assertIn('Methods single argument should be a one key dict',
                      next(self.g).message)

    def test_method_arguments_usage_1_3(self):
        self.loaded_package.format_version = '1.3'
        self.g = self.mpl_validator._valid_argument_usage('Standard')
        self.assertIn('Arguments usage is available since 1.4',
                      next(self.g).message)

    def test_wrong_method_arguments_usage(self):
        self.g = self.mpl_validator._valid_argument_usage('Standard1')
        self.assertIn('Usage is invalid value "Standard1"',
                      next(self.g).message)

    def test_method_arguments_invalid_name(self):
        self.g = self.mpl_validator._valid_arguments(
            [{'a#': {'Contract': '$.int()'}}])
        self.assertIn('Invalid name of argument "a#"',
                      next(self.g).message)
