import pytest

from schematics.exceptions import DataError
from schematics.models import Model
from schematics.types import IntType, StringType
from schematics.types.compound import ListType, ModelType
from schematics.util import ImportStringError


def test_simple_embedded_models():
    class Location(Model):
        country_code = StringType()

    class Player(Model):
        id = IntType()
        location = ModelType(Location)

    p = Player(dict(id=1, location={"country_code": "US"}))

    assert p.id == 1
    assert p.location.country_code == "US"

    p.location = Location({"country_code": "IS"})

    assert isinstance(p.location, Location)
    assert p.location.country_code == "IS"


def test_simple_embedded_models_is_none():
    class Location(Model):
        country_code = StringType()

    class Player(Model):
        id = IntType()
        location = ModelType(Location)

    p = Player(dict(id=1))

    assert p.id == 1
    assert p.location is None


def test_simple_embedded_model_set_to_none():
    class Location(Model):
        country_code = StringType()

    class Player(Model):
        id = IntType()
        location = ModelType(Location)

    p = Player(dict(id=1))
    p.location = None

    assert p.id == 1
    assert p.location is None


def test_simple_embedded_model_is_none_within_listtype():
    class QuestionResources(Model):
        type = StringType()

    class Question(Model):
        id = StringType()
        resources = ModelType(QuestionResources)

    class QuestionPack(Model):
        id = StringType()
        questions = ListType(ModelType(Question))

    question_pack = QuestionPack(
        {
            "id": "1",
            "questions": [
                {
                    "id": "1",
                },
            ],
        }
    )

    assert question_pack.questions[0].resources is None


def test_raises_validation_error_on_init_with_partial_submodel():
    class User(Model):
        name = StringType(required=True)
        age = IntType(required=True)

    class Card(Model):
        user = ModelType(User)

    u = User({"name": "Arthur"})
    c = Card({"user": u})

    with pytest.raises(DataError):
        c.validate()


def test_model_type():
    class User(Model):
        name = StringType()

    class Card(Model):
        user = ModelType(User)

    c = Card({"user": {"name": "Doggy"}})
    assert isinstance(c.user, User)
    assert c.user.name == "Doggy"


def test_equality_with_embedded_models():
    class Location(Model):
        country_code = StringType()

    class Player(Model):
        id = IntType()
        location = ModelType(Location)

    p1 = Player(dict(id=1, location={"country_code": "US"}))
    p2 = Player(dict(id=1, location={"country_code": "US"}))

    assert id(p1.location) != id(p2.location)
    assert p1.location == p2.location

    assert p1 == p2


def test_default_value_when_embedded_model():
    class Question(Model):
        question_id = StringType(required=True)

        type = StringType(default="text")

    class QuestionPack(Model):

        question = ModelType(Question)

    pack = QuestionPack({"question": {"question_id": 1}})

    assert pack.question.question_id == "1"
    assert pack.question.type == "text"


def test_export_loop_with_subclassed_model():
    class Asset(Model):
        file_name = StringType()

    class S3Asset(Asset):
        bucket_name = StringType()

    class Product(Model):
        title = StringType()
        asset = ModelType(Asset)

    asset = S3Asset({"bucket_name": "assets_bucket", "file_name": "bar"})

    product = Product({"title": "baz", "asset": asset})

    primitive = product.to_primitive()
    assert "bucket_name" in primitive["asset"]

    native = product.to_native()
    assert "bucket_name" in native["asset"]


def test_conversion_error_recursive_overhead():
    conversions = [0]

    class Leaf(Model):
        pass

    next_model = Leaf
    data = "not a mapping"

    for i in range(20):

        class Recursive(Model):
            x = ModelType(next_model, required=True)

            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                conversions[0] += 1
                assert conversions[0] < 25

        next_model = Recursive
        data = {"x": data}

    with pytest.raises(DataError):
        next_model(data)


def test_mock_object():
    class User(Model):
        name = StringType(required=True)
        age = IntType(required=True)

    assert ModelType(User, required=True).mock() is not None


def test_specify_model_by_name():
    class M(Model):
        to_one = ModelType("M")
        to_many = ListType(ModelType("M"))
        matrix = ListType(ListType(ModelType("M")))

    assert M.to_one.model_class is M
    assert M.to_many.field.model_class is M
    assert M.matrix.field.field.model_class is M


def test_model_context_pass_to_type():
    from schematics.datastructures import Context
    from schematics.types import BaseType

    class CustomType(BaseType):
        def to_native(self, value, context=None):
            suffix = context.suffix
            return str(value) + suffix

        def to_primitive(self, value, context=None):
            suffix = context.suffix
            return value[: -len(suffix)]

    class Thing(Model):
        x = CustomType()

    context = {"suffix": "z"}
    input = {"x": "thingie"}
    thing = Thing(input, context=context)
    assert thing.x == "thingiez"
    assert thing.to_primitive(context=context) == {"x": "thingie"}
    # try it with a Context object
    model_context = Context(suffix="z!")
    thing2 = Thing(input, context=model_context)
    assert thing2.x == "thingiez!"
    export_context = Context(suffix="z!")
    assert thing2.to_primitive(context=export_context) == {"x": "thingie"}
    with pytest.raises(AttributeError):
        # can't reuse the same Context object as was used for model
        # TODO this may be unexpected to the uninitiated; a custom exception
        #      could explain it better.
        thing2.to_primitive(context=model_context)


def test_model_app_data_pass_to_type():
    from schematics.types import BaseType

    class CustomType(BaseType):
        def to_native(self, value, context=None):
            suffix = context.app_data["suffix"]
            return str(value) + suffix

        def to_primitive(self, value, context=None):
            suffix = context.app_data["suffix"]
            return value[: -len(suffix)]

    class Thing(Model):
        x = CustomType()

    app_data = {"suffix": "z"}
    input = {"x": "thingie"}
    thing = Thing(input, app_data=app_data)
    assert thing.x == "thingiez"
    assert thing.to_primitive(app_data=app_data) == {"x": "thingie"}


class OuterModel:
    class InnerModel(Model):
        test = StringType()


def test_deep_string_search():
    class TestModel(Model):
        deep_model = ModelType("test_model_type.OuterModel.InnerModel")

    test = TestModel(dict(deep_model=dict(test="Abc")))
    assert test.validate() is None

    class TestModel2(Model):
        invalid_model = ModelType("a.c.d.e")

    with pytest.raises(ImportStringError):
        TestModel2(dict(invalid_model=dict(a="1")))


def test_recursive_string_self_reference():
    class RecursiveTestModel(Model):
        recursive_model = ModelType("RecursiveTestModel")
        test = StringType()

    test = RecursiveTestModel(dict(recursive_model=dict(test="Abc")))
    assert test.validate() is None
    assert test.recursive_model.test == "Abc"


def test_circular_string_reference():
    class TestModel1(Model):
        model2 = ModelType("TestModel2")
        name = StringType()

    class TestModel2(Model):
        model1 = ModelType("TestModel1")
        description = StringType()

    data1 = {"name": "Test1"}
    data2 = {"description": "Test2", "model1": data1}

    # TODO: we might want to support locals import late binding someday/somehow
    with pytest.raises(ImportStringError):
        test = TestModel1({"name": "Root", "model2": data2})
