# Copyright (c) 2015 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.

import datetime
import functools

import mock
import six

import glance.common.exception as exc
from glance.common.glare import declarative
import glance.common.glare.definitions as defs
from glance.common.glare import serialization
import glance.tests.utils as test_utils


BASE = declarative.get_declarative_base()


class TestDeclarativeProperties(test_utils.BaseTestCase):
    def test_artifact_type_properties(self):
        class SomeTypeWithNoExplicitName(BASE):
            some_attr = declarative.AttributeDefinition()

        class InheritedType(SomeTypeWithNoExplicitName):
            __type_version__ = '1.0'
            __type_name__ = 'ExplicitName'
            __type_description__ = 'Type description'
            __type_display_name__ = 'EXPLICIT_NAME'
            __endpoint__ = 'some_endpoint'

            some_attr = declarative.AttributeDefinition(display_name='NAME')

        base_type = SomeTypeWithNoExplicitName
        base_instance = SomeTypeWithNoExplicitName()
        self.assertIsNotNone(base_type.metadata)
        self.assertIsNotNone(base_instance.metadata)
        self.assertEqual(base_type.metadata, base_instance.metadata)
        self.assertEqual("SomeTypeWithNoExplicitName",
                         base_type.metadata.type_name)
        self.assertEqual("SomeTypeWithNoExplicitName",
                         base_type.metadata.type_display_name)
        self.assertEqual("1.0", base_type.metadata.type_version)
        self.assertIsNone(base_type.metadata.type_description)
        self.assertEqual('sometypewithnoexplicitname',
                         base_type.metadata.endpoint)

        self.assertIsNone(base_instance.some_attr)
        self.assertIsNotNone(base_type.some_attr)
        self.assertEqual(base_type.some_attr,
                         base_instance.metadata.attributes.all['some_attr'])
        self.assertEqual('some_attr', base_type.some_attr.name)
        self.assertEqual('some_attr', base_type.some_attr.display_name)
        self.assertIsNone(base_type.some_attr.description)

        derived_type = InheritedType
        derived_instance = InheritedType()

        self.assertIsNotNone(derived_type.metadata)
        self.assertIsNotNone(derived_instance.metadata)
        self.assertEqual(derived_type.metadata, derived_instance.metadata)
        self.assertEqual('ExplicitName', derived_type.metadata.type_name)
        self.assertEqual('EXPLICIT_NAME',
                         derived_type.metadata.type_display_name)
        self.assertEqual('1.0', derived_type.metadata.type_version)
        self.assertEqual('Type description',
                         derived_type.metadata.type_description)
        self.assertEqual('some_endpoint', derived_type.metadata.endpoint)
        self.assertIsNone(derived_instance.some_attr)
        self.assertIsNotNone(derived_type.some_attr)
        self.assertEqual(derived_type.some_attr,
                         derived_instance.metadata.attributes.all['some_attr'])
        self.assertEqual('some_attr', derived_type.some_attr.name)
        self.assertEqual('NAME', derived_type.some_attr.display_name)

    def test_wrong_type_definition(self):
        def declare_wrong_type_version():
            class WrongType(BASE):
                __type_version__ = 'abc'  # not a semver

            return WrongType

        def declare_wrong_type_name():
            class WrongType(BASE):
                __type_name__ = 'a' * 256  # too long

            return WrongType

        self.assertRaises(exc.InvalidArtifactTypeDefinition,
                          declare_wrong_type_version)
        self.assertRaises(exc.InvalidArtifactTypeDefinition,
                          declare_wrong_type_name)

    def test_base_declarative_attributes(self):
        class TestType(BASE):
            defaulted = declarative.PropertyDefinition(default=42)
            read_only = declarative.PropertyDefinition(readonly=True)
            required_attr = declarative.PropertyDefinition(required=True)

        e = self.assertRaises(exc.InvalidArtifactPropertyValue, TestType)
        self.assertEqual('required_attr', e.name)
        self.assertIsNone(e.value)
        tt = TestType(required_attr="universe")
        self.assertEqual('universe', tt.required_attr)
        self.assertEqual(42, tt.defaulted)
        self.assertIsNone(tt.read_only)

        tt = TestType(required_attr="universe", defaulted=0, read_only="Hello")
        self.assertEqual(0, tt.defaulted)
        self.assertEqual("Hello", tt.read_only)

        tt.defaulted = 5
        self.assertEqual(5, tt.defaulted)
        tt.required_attr = 'Foo'
        self.assertEqual('Foo', tt.required_attr)

        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'read_only', 'some_val')
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'required_attr', None)

        # no type checks in base AttributeDefinition
        o = object()
        tt.required_attr = o
        self.assertEqual(o, tt.required_attr)

    def test_generic_property(self):
        class TestType(BASE):
            simple_prop = declarative.PropertyDefinition()
            immutable_internal = declarative.PropertyDefinition(mutable=False,
                                                                internal=True)
            prop_with_allowed = declarative.PropertyDefinition(
                allowed_values=["Foo", True, 42])

        class DerivedType(TestType):
            prop_with_allowed = declarative.PropertyDefinition(
                allowed_values=["Foo", True, 42], required=True, default=42)

        tt = TestType()
        self.assertEqual(True,
                         tt.metadata.attributes.all['simple_prop'].mutable)
        self.assertEqual(False,
                         tt.metadata.attributes.all['simple_prop'].internal)
        self.assertEqual(False,
                         tt.metadata.attributes.all[
                             'immutable_internal'].mutable)
        self.assertEqual(True,
                         tt.metadata.attributes.all[
                             'immutable_internal'].internal)
        self.assertIsNone(tt.prop_with_allowed)
        tt = TestType(prop_with_allowed=42)
        self.assertEqual(42, tt.prop_with_allowed)
        tt = TestType(prop_with_allowed=True)
        self.assertEqual(True, tt.prop_with_allowed)
        tt = TestType(prop_with_allowed='Foo')
        self.assertEqual('Foo', tt.prop_with_allowed)

        tt.prop_with_allowed = 42
        self.assertEqual(42, tt.prop_with_allowed)
        tt.prop_with_allowed = 'Foo'
        self.assertEqual('Foo', tt.prop_with_allowed)
        tt.prop_with_allowed = True
        self.assertEqual(True, tt.prop_with_allowed)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr,
                          tt, 'prop_with_allowed', 'bar')
        # ensure that wrong assignment didn't change the value
        self.assertEqual(True, tt.prop_with_allowed)
        self.assertRaises(exc.InvalidArtifactPropertyValue, TestType,
                          prop_with_allowed=False)

        dt = DerivedType()
        self.assertEqual(42, dt.prop_with_allowed)

    def test_default_violates_allowed(self):
        def declare_wrong_type():
            class WrongType(BASE):
                prop = declarative.PropertyDefinition(
                    allowed_values=['foo', 'bar'],
                    default='baz')

            return WrongType

        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          declare_wrong_type)

    def test_string_property(self):
        class TestType(BASE):
            simple = defs.String()
            with_length = defs.String(max_length=10, min_length=5)
            with_pattern = defs.String(pattern='^\\d+$', default='42')

        tt = TestType()
        tt.simple = 'foo'
        self.assertEqual('foo', tt.simple)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr,
                          tt, 'simple', 42)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr,
                          tt, 'simple', 'x' * 256)
        self.assertRaises(exc.InvalidArtifactPropertyValue, TestType,
                          simple='x' * 256)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr,
                          tt, 'with_length', 'x' * 11)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr,
                          tt, 'with_length', 'x' * 4)
        tt.simple = 'x' * 5
        self.assertEqual('x' * 5, tt.simple)
        tt.simple = 'x' * 10
        self.assertEqual('x' * 10, tt.simple)

        self.assertEqual("42", tt.with_pattern)
        tt.with_pattern = '0'
        self.assertEqual('0', tt.with_pattern)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'with_pattern', 'abc')
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'with_pattern', '.123.')

    def test_binary_object_mutable(self):
        def declare_blob(mutable):
            class BLOB(BASE):
                prop = defs.BinaryObject(mutable=mutable)

            return BLOB

        blob = declare_blob(False)()
        self.assertFalse(blob.metadata.attributes.all['prop'].mutable)
        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          functools.partial(declare_blob, True))

    def test_default_and_allowed_violates_string_constrains(self):
        def declare_wrong_default():
            class WrongType(BASE):
                prop = defs.String(min_length=4, default='foo')

            return WrongType

        def declare_wrong_allowed():
            class WrongType(BASE):
                prop = defs.String(min_length=4, allowed_values=['foo', 'bar'])

            return WrongType

        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          declare_wrong_default)
        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          declare_wrong_allowed)

    def test_integer_property(self):
        class TestType(BASE):
            simple = defs.Integer()
            constrained = defs.Integer(min_value=10, max_value=50)

        tt = TestType()
        self.assertIsNone(tt.simple)
        self.assertIsNone(tt.constrained)

        tt.simple = 0
        tt.constrained = 10
        self.assertEqual(0, tt.simple)
        self.assertEqual(10, tt.constrained)

        tt.constrained = 50
        self.assertEqual(50, tt.constrained)

        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'constrained', 1)
        self.assertEqual(50, tt.constrained)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'constrained', 51)
        self.assertEqual(50, tt.constrained)

        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'simple', '11')
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'simple', 10.5)

    def test_default_and_allowed_violates_int_constrains(self):
        def declare_wrong_default():
            class WrongType(BASE):
                prop = defs.Integer(min_value=4, default=1)

            return WrongType

        def declare_wrong_allowed():
            class WrongType(BASE):
                prop = defs.Integer(min_value=4, max_value=10,
                                    allowed_values=[1, 15])

            return WrongType

        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          declare_wrong_default)
        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          declare_wrong_allowed)

    def test_numeric_values(self):
        class TestType(BASE):
            simple = defs.Numeric()
            constrained = defs.Numeric(min_value=3.14, max_value=4.1)

        tt = TestType(simple=0.1, constrained=4)
        self.assertEqual(0.1, tt.simple)
        self.assertEqual(4.0, tt.constrained)

        tt.simple = 1
        self.assertEqual(1, tt.simple)
        tt.constrained = 3.14
        self.assertEqual(3.14, tt.constrained)
        tt.constrained = 4.1
        self.assertEqual(4.1, tt.constrained)

        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'simple', 'qwerty')
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'constrained', 3)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          'constrained', 5)

    def test_default_and_allowed_violates_numeric_constrains(self):
        def declare_wrong_default():
            class WrongType(BASE):
                prop = defs.Numeric(min_value=4.0, default=1.1)

            return WrongType

        def declare_wrong_allowed():
            class WrongType(BASE):
                prop = defs.Numeric(min_value=4.0, max_value=10.0,
                                    allowed_values=[1.0, 15.5])

            return WrongType

        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          declare_wrong_default)
        self.assertRaises(exc.InvalidArtifactTypePropertyDefinition,
                          declare_wrong_allowed)

    def test_same_item_type_array(self):
        class TestType(BASE):
            simple = defs.Array()
            unique = defs.Array(unique=True)
            simple_with_allowed_values = defs.Array(
                defs.String(allowed_values=["Foo", "Bar"]))
            defaulted = defs.Array(defs.Boolean(), default=[True, False])
            constrained = defs.Array(item_type=defs.Numeric(min_value=0),
                                     min_size=3, max_size=5, unique=True)

        tt = TestType(simple=[])
        self.assertEqual([], tt.simple)
        tt.simple.append("Foo")
        self.assertEqual(["Foo"], tt.simple)
        tt.simple.append("Foo")
        self.assertEqual(["Foo", "Foo"], tt.simple)
        self.assertEqual(2, len(tt.simple))
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.simple.append,
                          42)
        tt.simple.pop(1)
        self.assertEqual(["Foo"], tt.simple)
        del tt.simple[0]
        self.assertEqual(0, len(tt.simple))

        tt.simple_with_allowed_values = ["Foo"]
        tt.simple_with_allowed_values.insert(0, "Bar")
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.simple_with_allowed_values.append, "Baz")

        self.assertEqual([True, False], tt.defaulted)
        tt.defaulted.pop()
        self.assertEqual([True], tt.defaulted)
        tt2 = TestType()
        self.assertEqual([True, False], tt2.defaulted)

        self.assertIsNone(tt.constrained)
        tt.constrained = [10, 5, 4]
        self.assertEqual([10, 5, 4], tt.constrained)
        tt.constrained[1] = 15
        self.assertEqual([10, 15, 4], tt.constrained)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.constrained.__setitem__, 1, -5)
        self.assertEqual([10, 15, 4], tt.constrained)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.constrained.remove, 15)
        self.assertEqual([10, 15, 4], tt.constrained)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.constrained.__delitem__, 1)
        self.assertEqual([10, 15, 4], tt.constrained)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.constrained.append, 15)
        self.assertEqual([10, 15, 4], tt.constrained)

        tt.unique = []
        tt.unique.append("foo")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.unique.append,
                          "foo")

    def test_tuple_style_array(self):
        class TestType(BASE):
            address = defs.Array(
                item_type=[defs.String(20), defs.Integer(min_value=1),
                           defs.Boolean()])

        tt = TestType(address=["Hope Street", 1234, True])
        self.assertEqual("Hope Street", tt.address[0])
        self.assertEqual(1234, tt.address[1])
        self.assertEqual(True, tt.address[2])

        # On Python 3, sort() fails because int (1) and string ("20") are not
        # comparable
        if six.PY2:
            self.assertRaises(exc.InvalidArtifactPropertyValue,
                              tt.address.sort)
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.pop, 0)
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.pop, 1)
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.pop)
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.append,
                          "Foo")

    def test_same_item_type_dict(self):
        class TestType(BASE):
            simple_props = defs.Dict()
            constrained_props = defs.Dict(
                properties=defs.Integer(min_value=1, allowed_values=[1, 2]),
                min_properties=2,
                max_properties=3)

        tt = TestType()
        self.assertIsNone(tt.simple_props)
        self.assertIsNone(tt.constrained_props)
        tt.simple_props = {}
        self.assertEqual({}, tt.simple_props)
        tt.simple_props["foo"] = "bar"
        self.assertEqual({"foo": "bar"}, tt.simple_props)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.simple_props.__setitem__, 42, "foo")
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.simple_props.setdefault, "bar", 42)

        tt.constrained_props = {"foo": 1, "bar": 2}
        self.assertEqual({"foo": 1, "bar": 2}, tt.constrained_props)
        tt.constrained_props["baz"] = 1
        self.assertEqual({"foo": 1, "bar": 2, "baz": 1}, tt.constrained_props)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.constrained_props.__setitem__, "foo", 3)
        self.assertEqual(1, tt.constrained_props["foo"])
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.constrained_props.__setitem__, "qux", 2)
        tt.constrained_props.pop("foo")
        self.assertEqual({"bar": 2, "baz": 1}, tt.constrained_props)
        tt.constrained_props['qux'] = 2
        self.assertEqual({"qux": 2, "bar": 2, "baz": 1}, tt.constrained_props)
        tt.constrained_props.popitem()
        dict_copy = tt.constrained_props.copy()
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.constrained_props.popitem)
        self.assertEqual(dict_copy, tt.constrained_props)

    def test_composite_dict(self):
        class TestType(BASE):
            props = defs.Dict(properties={"foo": defs.String(),
                                          "bar": defs.Boolean()})
            fixed = defs.Dict(properties={"name": defs.String(min_length=2),
                                          "age": defs.Integer(min_value=0,
                                                              max_value=99)})

        tt = TestType()
        tt.props = {"foo": "FOO", "bar": False}
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.props.__setitem__, "bar", 123)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.props.__setitem__, "extra", "value")
        tt.fixed = {"name": "Alex", "age": 42}
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.fixed.__setitem__, "age", 120)

    def test_immutables(self):
        class TestType(BASE):
            activated = defs.Boolean(required=True, default=False)
            name = defs.String(mutable=False)

            def __is_mutable__(self):
                return not self.activated

        tt = TestType()
        self.assertEqual(False, tt.activated)
        self.assertIsNone(tt.name)
        tt.name = "Foo"
        self.assertEqual("Foo", tt.name)
        tt.activated = True
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr,
                          tt, "name", "Bar")
        self.assertEqual("Foo", tt.name)
        tt.activated = False
        tt.name = "Bar"
        self.assertEqual("Bar", tt.name)

    def test_readonly_array_dict(self):
        class TestType(BASE):
            arr = defs.Array(readonly=True)
            dict = defs.Dict(readonly=True)

        tt = TestType(arr=["Foo", "Bar"], dict={"qux": "baz"})
        self.assertEqual(["Foo", "Bar"], tt.arr)
        self.assertEqual({"qux": "baz"}, tt.dict)
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append,
                          "Baz")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.insert,
                          0, "Baz")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.__setitem__,
                          0, "Baz")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.remove,
                          "Foo")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.pop)
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.dict.pop,
                          "qux")
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.dict.__setitem__, "qux", "foo")

    def test_mutable_array_dict(self):
        class TestType(BASE):
            arr = defs.Array(mutable=False)
            dict = defs.Dict(mutable=False)
            activated = defs.Boolean()

            def __is_mutable__(self):
                return not self.activated

        tt = TestType()
        tt.arr = []
        tt.dict = {}
        tt.arr.append("Foo")
        tt.arr.insert(0, "Bar")
        tt.dict["baz"] = "qux"
        tt.activated = True
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append,
                          "Baz")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.insert,
                          0, "Baz")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.__setitem__,
                          0, "Baz")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.remove,
                          "Foo")
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.pop)
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.dict.pop,
                          "qux")
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.dict.__setitem__, "qux", "foo")

    def test_readonly_as_write_once(self):
        class TestType(BASE):
            prop = defs.String(readonly=True)
            arr = defs.Array(readonly=True)

        tt = TestType()
        self.assertIsNone(tt.prop)
        tt.prop = "Foo"
        self.assertEqual("Foo", tt.prop)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt,
                          "prop", "bar")
        tt2 = TestType()
        self.assertIsNone(tt2.prop)
        tt2.prop = None
        self.assertIsNone(tt2.prop)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt2,
                          "prop", None)
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt2,
                          "prop", "foo")
        self.assertIsNone(tt.arr)
        tt.arr = ["foo", "bar"]
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append,
                          'baz')
        self.assertIsNone(tt2.arr)
        tt2.arr = None
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append,
                          'baz')


class TestArtifactType(test_utils.BaseTestCase):
    def test_create_artifact(self):
        a = defs.ArtifactType(**get_artifact_fixture())
        self.assertIsNotNone(a)
        self.assertEqual("123", a.id)
        self.assertEqual("ArtifactType", a.type_name)
        self.assertEqual("1.0", a.type_version)
        self.assertEqual("11.2", a.version)
        self.assertEqual("Foo", a.name)
        self.assertEqual("private", a.visibility)
        self.assertEqual("creating", a.state)
        self.assertEqual("my_tenant", a.owner)
        self.assertEqual(a.created_at, a.updated_at)
        self.assertIsNone(a.description)
        self.assertIsNone(a.published_at)
        self.assertIsNone(a.deleted_at)

        self.assertIsNone(a.description)

        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, "id",
                          "foo")
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a,
                          "state", "active")
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a,
                          "owner", "some other")
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a,
                          "created_at", datetime.datetime.now())
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a,
                          "deleted_at", datetime.datetime.now())
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a,
                          "updated_at", datetime.datetime.now())
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a,
                          "published_at", datetime.datetime.now())
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a,
                          "visibility", "wrong")

    def test_dependency_prop(self):
        class DerivedType(defs.ArtifactType):
            depends_on_any = defs.ArtifactReference()
            depends_on_self = defs.ArtifactReference(type_name='DerivedType')
            depends_on_self_version = defs.ArtifactReference(
                type_name='DerivedType',
                type_version='1.0')

        class DerivedTypeV11(DerivedType):
            __type_name__ = 'DerivedType'
            __type_version__ = '1.1'
            depends_on_self_version = defs.ArtifactReference(
                type_name='DerivedType',
                type_version='1.1')

        d1 = DerivedType(**get_artifact_fixture())
        d2 = DerivedTypeV11(**get_artifact_fixture())
        a = defs.ArtifactType(**get_artifact_fixture())
        d1.depends_on_any = a
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, d1,
                          'depends_on_self', a)
        d1.depends_on_self = d2
        d2.depends_on_self = d1
        d1.depends_on_self_version = d1
        d2.depends_on_self_version = d2
        self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, d1,
                          'depends_on_self_version', d2)

    def test_dependency_list(self):
        class FooType(defs.ArtifactType):
            pass

        class BarType(defs.ArtifactType):
            pass

        class TestType(defs.ArtifactType):
            depends_on = defs.ArtifactReferenceList()
            depends_on_self_or_foo = defs.ArtifactReferenceList(
                references=defs.ArtifactReference(['FooType', 'TestType']))

        a = defs.ArtifactType(**get_artifact_fixture(id="1"))
        a_copy = defs.ArtifactType(**get_artifact_fixture(id="1"))
        b = defs.ArtifactType(**get_artifact_fixture(id="2"))

        tt = TestType(**get_artifact_fixture(id="3"))
        foo = FooType(**get_artifact_fixture(id='4'))
        bar = BarType(**get_artifact_fixture(id='4'))

        tt.depends_on.append(a)
        tt.depends_on.append(b)
        self.assertEqual([a, b], tt.depends_on)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.depends_on.append, a)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.depends_on.append, a_copy)

        tt.depends_on_self_or_foo.append(tt)
        tt.depends_on_self_or_foo.append(foo)
        self.assertRaises(exc.InvalidArtifactPropertyValue,
                          tt.depends_on_self_or_foo.append, bar)
        self.assertEqual([tt, foo], tt.depends_on_self_or_foo)

    def test_blob(self):
        class TestType(defs.ArtifactType):
            image_file = defs.BinaryObject(max_file_size=201054,
                                           min_locations=1,
                                           max_locations=5)
            screen_shots = defs.BinaryObjectList(
                objects=defs.BinaryObject(min_file_size=100), min_count=1)

        tt = TestType(**get_artifact_fixture())
        blob = defs.Blob()
        blob.size = 1024
        blob.locations.append("file://some.file.path")
        tt.image_file = blob

        self.assertEqual(1024, tt.image_file.size)
        self.assertEqual(["file://some.file.path"], tt.image_file.locations)

    def test_pre_publish_blob_validation(self):
        class TestType(defs.ArtifactType):
            required_blob = defs.BinaryObject(required=True)
            optional_blob = defs.BinaryObject()

        tt = TestType(**get_artifact_fixture())
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.__pre_publish__)
        tt.required_blob = defs.Blob(size=0)
        tt.__pre_publish__()

    def test_pre_publish_dependency_validation(self):
        class TestType(defs.ArtifactType):
            required_dependency = defs.ArtifactReference(required=True)
            optional_dependency = defs.ArtifactReference()

        tt = TestType(**get_artifact_fixture())
        self.assertRaises(exc.InvalidArtifactPropertyValue, tt.__pre_publish__)
        tt.required_dependency = defs.ArtifactType(**get_artifact_fixture())
        tt.__pre_publish__()

    def test_default_value_of_immutable_field_in_active_state(self):
        class TestType(defs.ArtifactType):
            foo = defs.String(default='Bar', mutable=False)
        tt = TestType(**get_artifact_fixture(state='active'))
        self.assertEqual('Bar', tt.foo)


class SerTestType(defs.ArtifactType):
    some_string = defs.String()
    some_text = defs.Text()
    some_version = defs.SemVerString()
    some_int = defs.Integer()
    some_numeric = defs.Numeric()
    some_bool = defs.Boolean()
    some_array = defs.Array()
    another_array = defs.Array(
        item_type=[defs.Integer(), defs.Numeric(), defs.Boolean()])
    some_dict = defs.Dict()
    another_dict = defs.Dict(
        properties={'foo': defs.Integer(), 'bar': defs.Boolean()})
    some_ref = defs.ArtifactReference()
    some_ref_list = defs.ArtifactReferenceList()
    some_blob = defs.BinaryObject()
    some_blob_list = defs.BinaryObjectList()


class TestSerialization(test_utils.BaseTestCase):
    def test_serialization_to_db(self):
        ref1 = defs.ArtifactType(**get_artifact_fixture(id="1"))
        ref2 = defs.ArtifactType(**get_artifact_fixture(id="2"))
        ref3 = defs.ArtifactType(**get_artifact_fixture(id="3"))

        blob1 = defs.Blob(size=100, locations=['http://example.com/blob1'],
                          item_key='some_key', checksum='abc')
        blob2 = defs.Blob(size=200, locations=['http://example.com/blob2'],
                          item_key='another_key', checksum='fff')
        blob3 = defs.Blob(size=300, locations=['http://example.com/blob3'],
                          item_key='third_key', checksum='123')

        fixture = get_artifact_fixture()
        tt = SerTestType(**fixture)
        tt.some_string = 'bar'
        tt.some_text = 'bazz'
        tt.some_version = '11.22.33-beta'
        tt.some_int = 50
        tt.some_numeric = 10.341
        tt.some_bool = True
        tt.some_array = ['q', 'w', 'e', 'r', 't', 'y']
        tt.another_array = [1, 1.2, False]
        tt.some_dict = {'foobar': "FOOBAR", 'baz': "QUX"}
        tt.another_dict = {'foo': 1, 'bar': True}
        tt.some_ref = ref1
        tt.some_ref_list = [ref2, ref3]
        tt.some_blob = blob1
        tt.some_blob_list = [blob2, blob3]

        results = serialization.serialize_for_db(tt)
        expected = fixture
        expected['type_name'] = 'SerTestType'
        expected['type_version'] = '1.0'
        expected['properties'] = {
            'some_string': {
                'type': 'string',
                'value': 'bar'
            },
            'some_text': {
                'type': 'text',
                'value': 'bazz'
            },
            'some_version': {
                'type': 'string',
                'value': '11.22.33-beta'
            },
            'some_int': {
                'type': 'int',
                'value': 50
            },
            'some_numeric': {
                'type': 'numeric',
                'value': 10.341
            },
            'some_bool': {
                'type': 'bool',
                'value': True
            },
            'some_array': {
                'type': 'array',
                'value': [
                    {
                        'type': 'string',
                        'value': 'q'
                    },
                    {
                        'type': 'string',
                        'value': 'w'
                    },
                    {
                        'type': 'string',
                        'value': 'e'
                    },
                    {
                        'type': 'string',
                        'value': 'r'
                    },
                    {
                        'type': 'string',
                        'value': 't'
                    },
                    {
                        'type': 'string',
                        'value': 'y'
                    }
                ]
            },
            'another_array': {
                'type': 'array',
                'value': [
                    {
                        'type': 'int',
                        'value': 1
                    },
                    {
                        'type': 'numeric',
                        'value': 1.2
                    },
                    {
                        'type': 'bool',
                        'value': False
                    }
                ]
            },
            'some_dict.foobar': {
                'type': 'string',
                'value': 'FOOBAR'
            },
            'some_dict.baz': {
                'type': 'string',
                'value': 'QUX'
            },
            'another_dict.foo': {
                'type': 'int',
                'value': 1
            },
            'another_dict.bar': {
                'type': 'bool',
                'value': True
            }
        }
        expected['dependencies'] = {
            'some_ref': ['1'],
            'some_ref_list': ['2', '3']
        }
        expected['blobs'] = {
            'some_blob': [
                {
                    'size': 100,
                    'checksum': 'abc',
                    'item_key': 'some_key',
                    'locations': ['http://example.com/blob1']
                }],
            'some_blob_list': [
                {
                    'size': 200,
                    'checksum': 'fff',
                    'item_key': 'another_key',
                    'locations': ['http://example.com/blob2']
                },
                {
                    'size': 300,
                    'checksum': '123',
                    'item_key': 'third_key',
                    'locations': ['http://example.com/blob3']
                }
            ]
        }

        self.assertEqual(expected, results)

    def test_deserialize_from_db(self):
        ts = datetime.datetime.now()
        db_dict = {
            "type_name": 'SerTestType',
            "type_version": '1.0',
            "id": "123",
            "version": "11.2",
            "description": None,
            "name": "Foo",
            "visibility": "private",
            "state": "creating",
            "owner": "my_tenant",
            "created_at": ts,
            "updated_at": ts,
            "deleted_at": None,
            "published_at": None,
            "tags": ["test", "fixture"],
            "properties": {
                'some_string': {
                    'type': 'string',
                    'value': 'bar'
                },
                'some_text': {
                    'type': 'text',
                    'value': 'bazz'
                },
                'some_version': {
                    'type': 'string',
                    'value': '11.22.33-beta'
                },
                'some_int': {
                    'type': 'int',
                    'value': 50
                },
                'some_numeric': {
                    'type': 'numeric',
                    'value': 10.341
                },
                'some_bool': {
                    'type': 'bool',
                    'value': True
                },
                'some_array': {
                    'type': 'array',
                    'value': [
                        {
                            'type': 'string',
                            'value': 'q'
                        },
                        {
                            'type': 'string',
                            'value': 'w'
                        },
                        {
                            'type': 'string',
                            'value': 'e'
                        },
                        {
                            'type': 'string',
                            'value': 'r'
                        },
                        {
                            'type': 'string',
                            'value': 't'
                        },
                        {
                            'type': 'string',
                            'value': 'y'
                        }
                    ]
                },
                'another_array': {
                    'type': 'array',
                    'value': [
                        {
                            'type': 'int',
                            'value': 1
                        },
                        {
                            'type': 'numeric',
                            'value': 1.2
                        },
                        {
                            'type': 'bool',
                            'value': False
                        }
                    ]
                },
                'some_dict.foobar': {
                    'type': 'string',
                    'value': 'FOOBAR'
                },
                'some_dict.baz': {
                    'type': 'string',
                    'value': 'QUX'
                },
                'another_dict.foo': {
                    'type': 'int',
                    'value': 1
                },
                'another_dict.bar': {
                    'type': 'bool',
                    'value': True
                }
            },
            'blobs': {
                'some_blob': [
                    {
                        'size': 100,
                        'checksum': 'abc',
                        'item_key': 'some_key',
                        'locations': ['http://example.com/blob1']
                    }],
                'some_blob_list': [
                    {
                        'size': 200,
                        'checksum': 'fff',
                        'item_key': 'another_key',
                        'locations': ['http://example.com/blob2']
                    },
                    {
                        'size': 300,
                        'checksum': '123',
                        'item_key': 'third_key',
                        'locations': ['http://example.com/blob3']
                    }
                ]
            },
            'dependencies': {
                'some_ref': [
                    {
                        "type_name": 'ArtifactType',
                        "type_version": '1.0',
                        "id": "1",
                        "version": "11.2",
                        "description": None,
                        "name": "Foo",
                        "visibility": "private",
                        "state": "creating",
                        "owner": "my_tenant",
                        "created_at": ts,
                        "updated_at": ts,
                        "deleted_at": None,
                        "published_at": None,
                        "tags": ["test", "fixture"],
                        "properties": {},
                        "blobs": {},
                        "dependencies": {}
                    }
                ],
                'some_ref_list': [
                    {
                        "type_name": 'ArtifactType',
                        "type_version": '1.0',
                        "id": "2",
                        "version": "11.2",
                        "description": None,
                        "name": "Foo",
                        "visibility": "private",
                        "state": "creating",
                        "owner": "my_tenant",
                        "created_at": ts,
                        "updated_at": ts,
                        "deleted_at": None,
                        "published_at": None,
                        "tags": ["test", "fixture"],
                        "properties": {},
                        "blobs": {},
                        "dependencies": {}
                    },
                    {
                        "type_name": 'ArtifactType',
                        "type_version": '1.0',
                        "id": "3",
                        "version": "11.2",
                        "description": None,
                        "name": "Foo",
                        "visibility": "private",
                        "state": "creating",
                        "owner": "my_tenant",
                        "created_at": ts,
                        "updated_at": ts,
                        "deleted_at": None,
                        "published_at": None,
                        "tags": ["test", "fixture"],
                        "properties": {},
                        "blobs": {},
                        "dependencies": {}
                    }
                ]
            }
        }
        plugins_dict = {'SerTestType': [SerTestType],
                        'ArtifactType': [defs.ArtifactType]}

        def _retrieve_plugin(name, version):
            return next((p for p in plugins_dict.get(name, [])
                        if version and p.version == version),
                        plugins_dict.get(name, [None])[0])
        plugins = mock.Mock()
        plugins.get_class_by_typename = _retrieve_plugin
        art = serialization.deserialize_from_db(db_dict, plugins)
        self.assertEqual('123', art.id)
        self.assertEqual('11.2', art.version)
        self.assertIsNone(art.description)
        self.assertEqual('Foo', art.name)
        self.assertEqual('private', art.visibility)
        self.assertEqual('private', art.visibility)


def get_artifact_fixture(**kwargs):
    ts = datetime.datetime.now()
    fixture = {
        "id": "123",
        "version": "11.2",
        "description": None,
        "name": "Foo",
        "visibility": "private",
        "state": "creating",
        "owner": "my_tenant",
        "created_at": ts,
        "updated_at": ts,
        "deleted_at": None,
        "published_at": None,
        "tags": ["test", "fixture"]
    }
    fixture.update(kwargs)
    return fixture
