# Copyright 2015 The Kubernetes Authors 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.
# 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.

import os
import unittest
import schema_validation
import yaml

INVALID_PROPERTIES = "Invalid properties for 'template.py'"


def GetFilePath():
    """Find our source and data files."""
    return os.path.dirname(os.path.abspath(__file__))


def ReadTestFile(filename):
    """Returns contents of a file from the testdata/ directory."""

    full_path = os.path.join(GetFilePath(), '..', 'test', 'schemas', filename)
    return open(full_path, 'r').read()


def RawValidate(raw_properties, schema_name, raw_schema):
    return ImportsRawValidate(raw_properties, schema_name,
                              {schema_name: raw_schema})


def ImportsRawValidate(raw_properties, schema_name, import_map):
    """Takes raw properties, calls validate and returns yaml properties."""
    properties = yaml.safe_load(raw_properties)
    return schema_validation.Validate(properties, schema_name, 'template.py',
                                      import_map)


class SchemaValidationTest(unittest.TestCase):
    """Tests of the schema portion of the template expansion library."""

    def testDefaults(self):
        schema_name = 'defaults.jinja.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''
        expected_properties = """
            alpha: alpha
            one: 1
        """
        self.assertEqual(yaml.safe_load(expected_properties),
                         RawValidate(empty_properties, schema_name, schema))

    def testNestedDefaults(self):
        schema_name = 'nested_defaults.py.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            zone: us-central1-a
            disks:
                - name: backup           # diskType and sizeGb set by default
                - name: cache            # sizeGb set by default
                    diskType: pd-ssd
                - name: data             # Nothing set by default
                    diskType: pd-ssd
                    sizeGb: 150
                - name: swap             # diskType set by default
                    sizeGb: 200
        """
        expected_properties = """
            zone: us-central1-a
            disks:
                - sizeGb: 100
                    diskType: pd-standard
                    name: backup
                - sizeGb: 100
                    diskType: pd-ssd
                    name: cache
                - sizeGb: 150
                    diskType: pd-ssd
                    name: data
                - sizeGb: 200
                    diskType: pd-standard
                    name: swap
        """
        self.assertEqual(yaml.safe_load(expected_properties),
                         RawValidate(properties, schema_name, schema))

    def testNestedRefDefaults(self):
        schema_name = 'ref_nested_defaults.py.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            zone: us-central1-a
            disks:
                - name: backup           # diskType and sizeGb set by default
                - name: cache            # sizeGb set by default
                    diskType: pd-ssd
                - name: data             # Nothing set by default
                    diskType: pd-ssd
                    sizeGb: 150
                - name: swap             # diskType set by default
                    sizeGb: 200
        """
        expected_properties = """
            zone: us-central1-a
            disks:
                - sizeGb: 100
                    diskType: pd-standard
                    name: backup
                - sizeGb: 100
                    diskType: pd-ssd
                    name: cache
                - sizeGb: 150
                    diskType: pd-ssd
                    name: data
                - sizeGb: 200
                    diskType: pd-standard
                    name: swap
        """
        self.assertEqual(yaml.safe_load(expected_properties),
                         RawValidate(properties, schema_name, schema))

    def testInvalidDefault(self):
        schema_name = 'invalid_default.jinja.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''
        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn(INVALID_PROPERTIES, e.message)
            self.assertIn("'string' is not of type 'integer' at ['number']",
                          e.message)

    def testRequiredDefault(self):
        schema_name = 'required_default.jinja.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''
        expected_properties = """
            name: my_name
        """
        self.assertEqual(yaml.safe_load(expected_properties),
                         RawValidate(empty_properties, schema_name, schema))

    def testRequiredDefaultReference(self):
        schema_name = 'req_default_ref.py.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''

        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn(INVALID_PROPERTIES, e.message)
            self.assertIn("'my_name' is not of type 'integer' at ['number']",
                          e.message)

    def testDefaultReference(self):
        schema_name = 'default_ref.jinja.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''
        expected_properties = 'number: 1'

        self.assertEqual(yaml.safe_load(expected_properties),
                         RawValidate(empty_properties, schema_name, schema))

    def testMissingQuoteInReference(self):
        schema_name = 'missing_quote.py.schema'
        schema = ReadTestFile(schema_name)
        properties = 'number: 1'

        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(2, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn("type 'NoneType' is not iterable", e.message)
            self.assertIn('around your reference', e.message)

    def testRequiredPropertyMissing(self):
        schema_name = 'required.jinja.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''
        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn(INVALID_PROPERTIES, e.message)
            self.assertIn("'name' is a required property", e.errors[0].message)

    def testRequiredPropertyValid(self):
        schema_name = 'required.jinja.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            name: my-name
        """
        self.assertEqual(yaml.safe_load(properties),
                         RawValidate(properties, schema_name, schema))

    def testMultipleErrors(self):
        schema_name = 'defaults.py.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            one: not a number
            alpha: 12345
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(2, len(e.errors))
            self.assertIn(INVALID_PROPERTIES, e.message)
            self.assertIn("'not a number' is not of type 'integer' at ['one']",
                          e.message)
            self.assertIn("12345 is not of type 'string' at ['alpha']",
                          e.message)

    def testNumbersValid(self):
        schema_name = 'numbers.py.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            minimum0: 0
            exclusiveMin0: 1
            maximum10: 10
            exclusiveMax10: 9
            even: 20
            odd: 21
        """
        self.assertEquals(yaml.safe_load(properties),
                          RawValidate(properties, schema_name, schema))

    def testNumbersInvalid(self):
        schema_name = 'numbers.py.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            minimum0: -1
            exclusiveMin0: 0
            maximum10: 11
            exclusiveMax10: 10
            even: 21
            odd: 20
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(6, len(e.errors))
            self.assertIn(INVALID_PROPERTIES, e.message)
            self.assertIn("-1 is less than the minimum of 0 at ['minimum0']",
                          e.message)
            self.assertIn(('0 is less than or equal to the minimum of 0'
                           " at ['exclusiveMin0']"), e.message)
            self.assertIn("11 is greater than the maximum of 10 at \
                          ['maximum10']", e.message)
            self.assertIn(('10 is greater than or equal to the maximum of 10'
                           " at ['exclusiveMax10']"), e.message)
            self.assertIn("21 is not a multiple of 2 at ['even']", e.message)
            self.assertIn("{'multipleOf': 2} is not allowed for 20 at ['odd']",
                          e.message)

    def testReference(self):
        schema_name = 'reference.jinja.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            odd: 6
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn('even', e.message)
            self.assertIn('is not allowed for 6', e.message)

    def testBadSchema(self):
        schema_name = 'bad.jinja.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''
        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(2, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn("u'minimum' is a dependency of u'exclusiveMinimum'",
                          e.message)
            self.assertIn("0 is not of type u'boolean'", e.message)

    def testInvalidReference(self):
        schema_name = 'invalid_reference.py.schema'
        schema = ReadTestFile(schema_name)
        properties = 'odd: 1'
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn('Unresolvable JSON pointer', e.message)

    def testInvalidReferenceInSchema(self):
        schema_name = 'invalid_reference_schema.py.schema'
        schema = ReadTestFile(schema_name)
        empty_properties = ''
        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn('Unresolvable JSON pointer', e.message)

    def testMetadata(self):
        schema_name = 'metadata.py.schema'
        schema = ReadTestFile(schema_name)
        properties = """
            one: 2
            alpha: beta
        """
        self.assertEquals(yaml.safe_load(properties),
                          RawValidate(properties, schema_name, schema))

    def testInvalidInput(self):
        schema_name = 'schema'
        schema = """
            info:
                title: Invalid Input
            properties: invalid
        """
        properties = """
            one: 2
            alpha: beta
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn("'invalid' is not of type u'object'", e.message)

    def testPattern(self):
        schema_name = 'schema'
        schema = r"""
            properties:
                bad-zone:
                    pattern: \w+-\w+-\w+
                zone:
                    pattern: \w+-\w+-\w+
        """
        properties = """
            bad-zone: abc
            zone: us-central1-a
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn('Invalid properties', e.message)
            self.assertIn("'abc' does not match", e.message)
            self.assertIn('bad-zone', e.message)

    def testUniqueItems(self):
        schema_name = 'schema'
        schema = """
            properties:
                bad-list:
                    type: array
                    uniqueItems: true
                list:
                    type: array
                    uniqueItems: true
        """
        properties = """
            bad-list:
            - a
            - b
            - a
            list:
            - a
            - b
            - c
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn('Invalid properties', e.message)
            self.assertIn('has non-unique elements', e.message)
            self.assertIn('bad-list', e.message)

    def testUniqueItemsOnString(self):
        schema_name = 'schema'
        schema = """
            properties:
                ok-string:
                    type: string
                    uniqueItems: true
                string:
                    type: string
                    uniqueItems: true
        """
        properties = """
            ok-string: aaa
            string: abc
        """
        self.assertEquals(yaml.safe_load(properties),
                          RawValidate(properties, schema_name, schema))

    def testRequiredTopLevel(self):
        schema_name = 'schema'
        schema = """
            info:
                title: Invalid Input
            required:
                - name
        """
        properties = """
            one: 2
            alpha: beta
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn(INVALID_PROPERTIES, e.message)
            self.assertIn("'name' is a required property", e.message)

    def testEmptySchemaProperties(self):
        schema_name = 'schema'
        schema = """
            info:
                title: Empty Input
            properties:
        """
        properties = """
            one: 2
            alpha: beta
        """
        try:
            RawValidate(properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn("None is not of type u'object' at [u'properties']",
                          e.message)

    def testNoInput(self):
        schema = """
            info:
                title: No other sections
        """
        properties = """
            one: 2
            alpha: beta
        """
        self.assertEquals(yaml.safe_load(properties),
                          RawValidate(properties, 'schema', schema))

    def testEmptySchema(self):
        schema = ''
        properties = """
            one: 2
            alpha: beta
        """
        self.assertEquals(yaml.safe_load(properties),
                          RawValidate(properties, 'schema', schema))

    def testImportPathSchema(self):
        schema = """
            imports:
                - path: a
                - path: path/to/b
                    name: b
        """
        properties = """
            one: 2
            alpha: beta
        """

        import_map = {'schema': schema,
                      'a': '',
                      'b': ''}

        self.assertEquals(yaml.safe_load(properties),
                          ImportsRawValidate(properties, 'schema', import_map))

    def testImportSchemaMissing(self):
        schema = ''
        empty_properties = ''

        try:
            properties = yaml.safe_load(empty_properties)
            schema_validation.Validate(properties, 'schema', 'template',
                                       {'wrong_name': schema})
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Could not find schema file 'schema'", e.message)

    def testImportsMalformedNotAList(self):
        schema_name = 'schema'
        schema = """
            imports: not-a-list
            """
        empty_properties = ''

        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn("is not of type 'array' at ['imports']", e.message)

    def testImportsMalformedMissingPath(self):
        schema_name = 'schema'
        schema = """
            imports:
                - name: no_path.yaml
            """
        empty_properties = ''

        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn("'path' is a required property", e.message)

    def testImportsMalformedNonunique(self):
        schema_name = 'schema'
        schema = """
            imports:
                - path: a.yaml
                    name: a
                - path: a.yaml
                    name: a
            """
        empty_properties = ''

        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn('non-unique elements', e.message)

    def testImportsMalformedAdditionalProperties(self):
        schema_name = 'schema'
        schema = """
            imports:
                - path: a.yaml
                    gnome: a
            """
        empty_properties = ''

        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(1, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn('Additional properties are not allowed'
                          " ('gnome' was unexpected)", e.message)

    def testImportAndInputErrors(self):
        schema = """
            imports:
                - path: file
            required:
                - name
            """
        empty_properties = ''

        try:
            RawValidate(empty_properties, 'schema', schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(2, len(e.errors))
            self.assertIn("'file' requested in schema 'schema'", e.message)
            self.assertIn("'name' is a required property", e.message)

    def testImportAndInputSchemaErrors(self):
        schema_name = 'schema'
        schema = """
            imports: not-a-list
            required: not-a-list
            """
        empty_properties = ''

        try:
            RawValidate(empty_properties, schema_name, schema)
            self.fail('Validation should fail')
        except schema_validation.ValidationErrors as e:
            self.assertEqual(2, len(e.errors))
            self.assertIn("Invalid schema '%s'" % schema_name, e.message)
            self.assertIn("is not of type 'array' at ['imports']", e.message)
            self.assertIn("is not of type u'array' at [u'required']",
                          e.message)

if __name__ == '__main__':
    unittest.main()
