#    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 testtools import matchers

from toscaparser.common import exception
from toscaparser.elements.property_definition import PropertyDef
from toscaparser.nodetemplate import NodeTemplate
from toscaparser.properties import Property
from toscaparser.tests.base import TestCase
from toscaparser.utils.gettextutils import _
from toscaparser.utils import yamlparser


class PropertyTest(TestCase):

    def test_type(self):
        test_property_schema = {'type': 'string'}
        propertyInstance = Property('test_property', 'Hughes',
                                    test_property_schema)
        self.assertEqual('string', propertyInstance.type)

    def test_type_invalid(self):
        test_property_schema = {'type': 'Fish'}
        propertyInstance = Property('test_property', 'Hughes',
                                    test_property_schema)
        error = self.assertRaises(exception.InvalidTypeError,
                                  propertyInstance.validate)
        self.assertEqual(_('Type "Fish" is not a valid type.'), str(error))

    def test_list(self):
        test_property_schema = {'type': 'list'}
        propertyInstance = Property('test_property', ['a', 'b'],
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual(['a', 'b'], propertyInstance.value)

    def test_list_invalid(self):
        test_property_schema = {'type': 'list'}
        propertyInstance = Property('test_property', 'a',
                                    test_property_schema)
        error = self.assertRaises(ValueError, propertyInstance.validate)
        self.assertEqual(_('"a" is not a list.'), str(error))

    def test_list_entry_schema(self):
        test_property_schema = {'type': 'list',
                                'entry_schema': {'type': 'string'}}
        propertyInstance = Property('test_property', ['a', 'b'],
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual(['a', 'b'], propertyInstance.value)

        schema_snippet = '''
        type: list
        entry_schema:
          type: string
          constraints:
            - min_length: 2
        '''
        test_property_schema = yamlparser.simple_parse(schema_snippet)
        propertyInstance = Property('test_property', ['ab', 'cd'],
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual(['ab', 'cd'], propertyInstance.value)

    def test_list_entry_schema_invalid(self):
        test_property_schema = {'type': 'list',
                                'entry_schema': {'type': 'integer'}}
        propertyInstance = Property('test_property', [1, 'b'],
                                    test_property_schema)
        error = self.assertRaises(ValueError, propertyInstance.validate)
        self.assertEqual(_('"b" is not an integer.'), str(error))

    def test_map(self):
        test_property_schema = {'type': 'map'}
        propertyInstance = Property('test_property', {'a': 'b'},
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual({'a': 'b'}, propertyInstance.value)

    def test_map_invalid(self):
        test_property_schema = {'type': 'map'}
        propertyInstance = Property('test_property', 12,
                                    test_property_schema)
        error = self.assertRaises(ValueError, propertyInstance.validate)
        self.assertEqual(_('"12" is not a map.'), str(error))

    def test_map_entry_schema(self):
        test_property_schema = {'type': 'map',
                                'entry_schema': {'type': 'boolean'}}
        propertyInstance = Property('test_property',
                                    {'valid': True, 'required': True},
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual({'valid': True, 'required': True},
                         propertyInstance.value)

    def test_map_entry_schema_invalid(self):
        test_property_schema = {'type': 'map',
                                'entry_schema': {'type': 'boolean'}}
        propertyInstance = Property('test_property',
                                    {'valid': True, 'contact_name': 123},
                                    test_property_schema)
        error = self.assertRaises(ValueError, propertyInstance.validate)
        self.assertEqual(_('"123" is not a boolean.'), str(error))

    def test_boolean(self):
        test_property_schema = {'type': 'boolean'}
        propertyInstance = Property('test_property', 'true',
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        propertyInstance = Property('test_property', True,
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual(True, propertyInstance.value)

    def test_boolean_invalid(self):
        test_property_schema = {'type': 'boolean'}
        propertyInstance = Property('test_property', 12,
                                    test_property_schema)
        error = self.assertRaises(ValueError, propertyInstance.validate)
        self.assertEqual(_('"12" is not a boolean.'), str(error))

    def test_float(self):
        test_property_schema = {'type': 'float'}
        propertyInstance = Property('test_property', 0.1,
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual(0.1, propertyInstance.value)

    def test_float_invalid(self):
        test_property_schema = {'type': 'float'}
        propertyInstance = Property('test_property', 12,
                                    test_property_schema)
        error = self.assertRaises(ValueError, propertyInstance.validate)
        self.assertEqual(_('"12" is not a float.'), str(error))

    def test_timestamp(self):
        test_property_schema = {'type': 'timestamp'}
        # canonical timestamp
        propertyInstance = Property('test_property', '2015-04-01T02:59:43.1Z',
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual("2015-04-01T02:59:43.1Z", propertyInstance.value)

        # iso8601 timestamp
        propertyInstance = Property('test_property',
                                    '2015-04-01t21:59:43.10-05:00',
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual("2015-04-01t21:59:43.10-05:00",
                         propertyInstance.value)

        # space separated timestamp
        propertyInstance = Property('test_property',
                                    '2015-04-01 21:59:43.10 -5',
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual("2015-04-01 21:59:43.10 -5", propertyInstance.value)

        # no time zone timestamp
        propertyInstance = Property('test_property', '2015-04-01 21:59:43.10',
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual("2015-04-01 21:59:43.10", propertyInstance.value)

        # date (00:00:00Z)
        propertyInstance = Property('test_property', '2015-04-01',
                                    test_property_schema)
        self.assertIsNone(propertyInstance.validate())
        self.assertEqual("2015-04-01", propertyInstance.value)

    def test_timestamp_invalid(self):
        test_property_schema = {'type': 'timestamp'}
        # invalid timestamp - day out of range
        value = '2015-04-115T02:59:43.1Z'
        propertyInstance = Property('test_property', value,
                                    test_property_schema)
        error = self.assertRaises(ValueError, propertyInstance.validate)
        expected_message = (_('"%s" is not a valid timestamp.') % value)
        self.assertThat(str(error), matchers.StartsWith(expected_message))

    def test_required(self):
        test_property_schema = {'type': 'string'}
        propertyInstance = Property('test_property', 'Foo',
                                    test_property_schema)
        self.assertEqual(True, propertyInstance.required)

    def test_proprety_inheritance(self):

        tosca_custom_def = '''
          tosca.nodes.SoftwareComponent.MySoftware:
            derived_from: SoftwareComponent
            properties:
              install_path:
                required: false
                type: string
                default: /opt/mysoftware
        '''

        tosca_node_template = '''
          node_templates:
            mysoftware_instance:
              type: tosca.nodes.SoftwareComponent.MySoftware
              properties:
                component_version: 3.1
              requirements:
                - host: my_server_host

            my_server_host:
              type: tosca.nodes.Compute
        '''

        expected_properties = ['component_version',
                               'install_path']

        tpl = self._get_nodetemplate(tosca_node_template, tosca_custom_def)
        self.assertIsNone(tpl.validate())
        self.assertEqual(expected_properties,
                         sorted(tpl.get_properties().keys()))

    def test_missing_property_type(self):
        tpl_snippet = '''
         properties:
           prop:
             typo: tosca.mytesttype.Test
        '''
        schema = yamlparser.simple_parse(tpl_snippet)
        error = self.assertRaises(exception.InvalidSchemaError, PropertyDef,
                                  'prop', None, schema['properties']['prop'])
        self.assertEqual(_('Schema definition of "prop" must have a "type" '
                           'attribute.'), str(error))

    def test_invalid_required_value(self):
        tpl_snippet = '''
         properties:
           prop:
             type: tosca.mytesttype.Test
             required: dunno
        '''
        schema = yamlparser.simple_parse(tpl_snippet)
        error = self.assertRaises(exception.InvalidSchemaError, PropertyDef,
                                  'prop', None, schema['properties']['prop'])

        valid_values = ', '.join(PropertyDef.VALID_REQUIRED_VALUES)
        expected_message = (_('Schema definition of "prop" has "required" '
                              'attribute with invalid value "dunno". The '
                              'value must be one of "%s".') % valid_values)
        self.assertEqual(expected_message, str(error))

    def test_invalid_property_status(self):
        tpl_snippet = '''
         properties:
           prop:
             type: string
             status: unknown
        '''
        schema = yamlparser.simple_parse(tpl_snippet)
        error = self.assertRaises(exception.InvalidSchemaError, PropertyDef,
                                  'prop', None, schema['properties']['prop'])

        valid_values = ', '.join(PropertyDef.VALID_STATUS_VALUES)
        expected_message = (_('Schema definition of "prop" has "status" '
                              'attribute with invalid value "unknown". The '
                              'value must be one of "%s".') % valid_values)
        self.assertEqual(expected_message, str(error))

    def test_capability_proprety_inheritance(self):
        tosca_custom_def_example1 = '''
          tosca.capabilities.ScalableNew:
            derived_from: tosca.capabilities.Scalable
            properties:
              max_instances:
                type: integer
                default: 0
                required: no

          tosca.nodes.ComputeNew:
            derived_from: tosca.nodes.Compute
            capabilities:
              scalable:
                type: tosca.capabilities.ScalableNew
        '''

        tosca_node_template_example1 = '''
          node_templates:
            compute_instance:
              type: tosca.nodes.ComputeNew
              capabilities:
                scalable:
                  properties:
                    min_instances: 1
        '''

        tosca_custom_def_example2 = '''
          tosca.nodes.ComputeNew:
            derived_from: tosca.nodes.Compute
            capabilities:
              new_cap:
                type: tosca.capabilities.Scalable
        '''

        tosca_node_template_example2 = '''
          node_templates:
            db_server:
                type: tosca.nodes.ComputeNew
                capabilities:
                  host:
                   properties:
                     num_cpus: 1
        '''

        tpl1 = self._get_nodetemplate(tosca_node_template_example1,
                                      tosca_custom_def_example1)
        self.assertIsNone(tpl1.validate())

        tpl2 = self._get_nodetemplate(tosca_node_template_example2,
                                      tosca_custom_def_example2)
        self.assertIsNone(tpl2.validate())

    def _get_nodetemplate(self, tpl_snippet,
                          custom_def_snippet=None):
        nodetemplates = yamlparser.\
            simple_parse(tpl_snippet)['node_templates']
        custom_def = []
        if custom_def_snippet:
            custom_def = yamlparser.simple_parse(custom_def_snippet)
        name = list(nodetemplates.keys())[0]
        tpl = NodeTemplate(name, nodetemplates, custom_def)
        return tpl

    def test_explicit_relationship_proprety(self):

        tosca_node_template = '''
          node_templates:

            client_node:
              type: tosca.nodes.Compute
              requirements:
                - local_storage:
                    node: my_storage
                    relationship:
                      type: AttachesTo
                      properties:
                        location: /mnt/disk

            my_storage:
              type: tosca.nodes.BlockStorage
              properties:
                size: 1 GB
        '''

        expected_properties = ['location']

        nodetemplates = yamlparser.\
            simple_parse(tosca_node_template)['node_templates']
        tpl = NodeTemplate('client_node', nodetemplates, [])

        self.assertIsNone(tpl.validate())
        rel_tpls = []
        for relationship, trgt in tpl.relationships.items():
            rel_tpls.extend(trgt.get_relationship_template())
        self.assertEqual(expected_properties,
                         sorted(rel_tpls[0].get_properties().keys()))
