import jsonschema
import pytest

from ..schemaperfect import (UndefinedType, SchemaBase, Undefined, _FromDict,
                        SchemaValidationError)

# Make tests inherit from _TestSchema, so that when we test from_dict it won't
# try to use SchemaBase objects defined elsewhere as wrappers.

class _TestSchema(SchemaBase):
    @classmethod
    def _default_wrapper_classes(cls):
        return _TestSchema.__subclasses__()


class MySchema(_TestSchema):
    _schema = {
        'definitions': {
            'StringMapping': {'type': 'object', 'additionalProperties': {'type': 'string'}},
            'StringArray': {'type': 'array', 'items': {'type': 'string'}}
        },
        'properties': {
            'a': {'$ref': '#/definitions/StringMapping'},
            'a2': {'type': 'object', 'additionalProperties': {'type': 'number'}},
            'b': {'$ref': '#/definitions/StringArray'},
            'b2': {'type': 'array', 'items': {'type': 'number'}},
            'c': {'type': ['string', 'number']},
            'd': {'anyOf': [{'$ref': '#/definitions/StringMapping'},
                            {'$ref': '#/definitions/StringArray'}]}
        }
    }


class StringMapping(_TestSchema):
    _schema = {'$ref': '#/definitions/StringMapping'}
    _rootschema = MySchema._schema


class StringArray(_TestSchema):
    _schema = {'$ref': '#/definitions/StringArray'}
    _rootschema = MySchema._schema


class Derived(_TestSchema):
    _schema = {
        'definitions': {
            'Foo': {
                'type': 'object',
                'properties': {
                    'd': {'type': 'string'}
                }
            },
            'Bar': {'type': 'string', 'enum': ['A', 'B']}
        },
        'type': 'object',
        'additionalProperties': False,
        'properties': {
            'a': {'type': 'integer'},
            'b': {'type': 'string'},
            'c': {"$ref": "#/definitions/Foo"}
        }
    }


class Foo(_TestSchema):
    _schema = {"$ref": "#/definitions/Foo"}
    _rootschema = Derived._schema


class Bar(_TestSchema):
    _schema = {"$ref": "#/definitions/Bar"}
    _rootschema = Derived._schema


class SimpleUnion(_TestSchema):
    _schema = {'anyOf' : [{'type': 'integer'}, {'type': 'string'}]}


class DefinitionUnion(_TestSchema):
    _schema = {
        "anyOf": [
            {"$ref": "#/definitions/Foo"},
            {"$ref": "#/definitions/Bar"}
        ]
    }
    _rootschema = Derived._schema


class SimpleArray(_TestSchema):
    _schema = {
        'type': 'array',
        'items': {
            'anyOf' : [{'type': 'integer'}, {'type': 'string'}]
        }
    }


class InvalidProperties(_TestSchema):
    _schema = {
        'type': 'object',
        'properties': {
            'for': {},
            'as': {},
            'vega-lite': {},
            '$schema': {}
        }
    }


def test_construct_multifaceted_schema():
    dct = {'a': {'foo': 'bar'}, 'a2': {'foo': 42},
       'b': ['a', 'b', 'c'], 'b2': [1, 2, 3], 'c': 42,
       'd': ['x', 'y', 'z']}

    myschema = MySchema.from_dict(dct)
    assert myschema.to_dict() == dct

    myschema2 = MySchema(**dct)
    assert myschema2.to_dict() == dct

    assert isinstance(myschema.a, StringMapping)
    assert isinstance(myschema.a2, dict)
    assert isinstance(myschema.b, StringArray)
    assert isinstance(myschema.b2, list)
    assert isinstance(myschema.d, StringArray)


def test_schema_cases():
    assert Derived(a=4, b='yo').to_dict() == {'a': 4, 'b': 'yo'}
    assert Derived(a=4, c={'d': 'hey'}).to_dict() == {'a': 4, 'c': {'d': 'hey'}}
    assert Derived(a=4, b='5', c=Foo(d='val')).to_dict() == {'a': 4, 'b': '5', 'c': {'d': 'val'}}
    assert Foo(d='hello', f=4).to_dict() == {'d': 'hello', 'f': 4}

    assert Derived().to_dict() == {}
    assert Foo().to_dict() == {}

    with pytest.raises(jsonschema.ValidationError):
        # a needs to be an integer
        Derived(a='yo').to_dict()

    with pytest.raises(jsonschema.ValidationError):
        # Foo.d needs to be a string
        Derived(c=Foo(4)).to_dict()

    with pytest.raises(jsonschema.ValidationError):
        # no additional properties allowed
        Derived(foo='bar').to_dict()


def test_round_trip():
    D = {'a': 4, 'b': 'yo'}
    assert Derived.from_dict(D).to_dict() == D

    D = {'a': 4, 'c': {'d': 'hey'}}
    assert Derived.from_dict(D).to_dict() == D

    D = {'a': 4, 'b': '5', 'c': {'d': 'val'}}
    assert Derived.from_dict(D).to_dict() == D

    D = {'d': 'hello', 'f': 4}
    assert Foo.from_dict(D).to_dict() == D


def test_from_dict():
    D = {'a': 4, 'b': '5', 'c': {'d': 'val'}}
    obj = Derived.from_dict(D)
    assert obj.a == 4
    assert obj.b == '5'
    assert isinstance(obj.c, Foo)


def test_simple_type():
    assert SimpleUnion(4).to_dict() == 4


def test_simple_array():
    assert SimpleArray([4, 5, 'six']).to_dict() == [4, 5, 'six']
    assert SimpleArray.from_dict(list('abc')).to_dict() == list('abc')


def test_definition_union():
    obj = DefinitionUnion.from_dict("A")
    assert isinstance(obj, Bar)
    assert obj.to_dict() == "A"

    obj = DefinitionUnion.from_dict("B")
    assert isinstance(obj, Bar)
    assert obj.to_dict() == "B"

    obj = DefinitionUnion.from_dict({'d': 'yo'})
    assert isinstance(obj, Foo)
    assert obj.to_dict() == {'d': 'yo'}


def test_invalid_properties():
    dct = {'for': 2, 'as': 3, 'vega-lite': 4, '$schema': 5}
    invalid = InvalidProperties.from_dict(dct)
    assert invalid['for'] == 2
    assert invalid['as'] == 3
    assert invalid['vega-lite'] == 4
    assert invalid['$schema'] == 5
    assert invalid.to_dict() == dct


def test_undefined_singleton():
    assert Undefined is UndefinedType()


def test_copy():
    dct = {'a': {'foo': 'bar'}, 'a2': {'foo': 42},
       'b': ['a', 'b', 'c'], 'b2': [1, 2, 3], 'c': 42,
       'd': ['x', 'y', 'z']}

    myschema = MySchema.from_dict(dct)

    # Make sure copy is deep
    copy = myschema.copy(deep=True)
    copy['a']['foo'] = 'new value'
    copy['b'] = ['A', 'B', 'C']
    copy['c'] = 164
    assert myschema.to_dict() == dct

    # If we exclude a value, changing the copy changes the original
    copy = myschema.copy(deep=True, exclude=['a'])
    copy['a']['foo'] = 'new value'
    copy['b'] = ['A', 'B', 'C']
    copy['c'] = 164
    mydct = myschema.to_dict()
    assert mydct['a']['foo'] == 'new value'
    assert mydct['b'][0] == dct['b'][0]
    assert mydct['c'] == dct['c']

    # If copy is not deep, then changing copy below top level changes original
    copy = myschema.copy(deep=False)
    copy['a']['foo'] = 'baz'
    copy['b'] = ['A', 'B', 'C']
    copy['c'] = 164
    mydct = myschema.to_dict()
    assert mydct['a']['foo'] == 'baz'
    assert mydct['b'] == dct['b']
    assert mydct['c'] == dct['c']


def test_attribute_error():
    m = MySchema()
    with pytest.raises(AttributeError) as err:
        m.invalid_attribute
    assert str(err.value) == ("'MySchema' object has no attribute "
                              "'invalid_attribute'")


def test_to_from_json():
    dct = {'a': {'foo': 'bar'}, 'a2': {'foo': 42},
           'b': ['a', 'b', 'c'], 'b2': [1, 2, 3], 'c': 42,
           'd': ['x', 'y', 'z']}
    json_str = MySchema.from_dict(dct).to_json()
    new_dct = MySchema.from_json(json_str).to_dict()

    assert new_dct == dct


def test_class_with_no_schema():
    class BadSchema(SchemaBase):
        pass

    with pytest.raises(ValueError) as err:
        BadSchema(4)
    assert str(err.value).startswith("Cannot instantiate object")


@pytest.mark.parametrize('use_json', [True, False])
def test_hash_schema(use_json):
    classes = _TestSchema._default_wrapper_classes()

    for cls in classes:
        hsh1 = _FromDict.hash_schema(cls._schema, use_json=use_json)
        hsh2 = _FromDict.hash_schema(cls._schema, use_json=use_json)
        assert hsh1 == hsh2
        assert hash(hsh1) == hash(hsh2)


def test_schema_validation_error():
    try:
        MySchema(a={'foo': 4})
        the_err = None
    except jsonschema.ValidationError as err:
        the_err = err

    assert isinstance(the_err, SchemaValidationError)
    message = str(the_err)

    assert message.startswith('Invalid specification')
    assert 'test_schemaperfect.MySchema->a' in message
    assert "validating {!r}".format(the_err.validator) in message
    assert the_err.message in message
