from dataclasses import dataclass, field
from typing import List, Optional, Union
from uuid import UUID

from .conftest import (
    Foo,
    Point,
    Recursive,
    OpaqueData,
    ShoppingCart,
    Product,
    ProductList,
    SubSchemas,
    Bar,
    Weekday,
    JsonSchemaMixin,
    Zoo,
    Baz,
)
import pytest

from hologram import ValidationError


FOO_SCHEMA = {
    "description": "A foo that foos",
    "properties": {
        "a": {"format": "date-time", "type": "string"},
        "b": {
            "oneOf": [
                {"items": {"$ref": "#/definitions/Point"}, "type": "array"},
                {"type": "null"},
            ]
        },
        "c": {"additionalProperties": {"type": "integer"}, "type": "object"},
        "d": {
            "type": "string",
            "enum": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
        },
        "f": {
            "type": "array",
            "minItems": 2,
            "maxItems": 2,
            "items": [{"type": "string"}, {"type": "integer"}],
        },
        "g": {"type": "array", "items": {"type": "string"}},
        "e": {
            "oneOf": [
                {"type": "string", "minLength": 5, "maxLength": 8},
                {"type": "null"},
            ]
        },
        "h": {"oneOf": [{"$ref": "#/definitions/Point"}, {"type": "null"}]},
    },
    "type": "object",
    "required": ["a", "c", "d", "f", "g"],
    "additionalProperties": False,
}

# Fixme: Fields in description no longer match
POINT_SCHEMA = {
    "description": Point.__doc__,
    "type": "object",
    "properties": {
        "z": {"type": "number", "description": "Point x coordinate"},
        "y": {"type": "number", "description": "Point y coordinate"},
    },
    "required": ["z", "y"],
    "additionalProperties": False,
}

RECURSIVE_SCHEMA = {
    "description": Recursive.__doc__,
    "properties": {
        "a": {"type": "string"},
        "b": {"$ref": "#/definitions/Recursive"},
    },
    "type": "object",
    "required": ["a"],
    "additionalProperties": False,
}

OPAQUE_DATA_SCHEMA = {
    "description": OpaqueData.__doc__,
    "properties": {"a": {"type": "array"}, "b": {"type": "object"}},
    "type": "object",
    "required": ["a", "b"],
    "additionalProperties": False,
}

PRODUCT_SCHEMA = {
    "description": Product.__doc__,
    "properties": {
        "cost": {"type": "number", "default": 20.0},
        "name": {"type": "string"},
    },
    "required": ["name"],
    "type": "object",
    "additionalProperties": False,
}

SHOPPING_CART_SCHEMA = {
    "description": ShoppingCart.__doc__,
    "properties": {
        "items": {"items": {"$ref": "#/definitions/Product"}, "type": "array"}
    },
    "required": ["items"],
    "type": "object",
    "additionalProperties": False,
}
PRODUCT_LIST_SCHEMA = {
    "description": ProductList.__doc__,
    "properties": {
        "products": {
            "additionalProperties": {"$ref": "#/definitions/Product"},
            "type": "object",
        }
    },
    "type": "object",
    "required": ["products"],
    "additionalProperties": False,
}
BAR_SCHEMA = {
    "type": "object",
    "description": "Type with union field",
    "properties": {
        "a": {
            "oneOf": [
                {
                    "type": "string",
                    "enum": [
                        "Monday",
                        "Tuesday",
                        "Wednesday",
                        "Thursday",
                        "Friday",
                    ],
                },
                {"$ref": "#/definitions/Point"},
            ]
        }
    },
    "required": ["a"],
    "additionalProperties": False,
}
ZOO_SCHEMA = {
    "type": "object",
    "description": "A zoo",
    "properties": {
        "animal_types": {
            "additionalProperties": {"type": "string"},
            "type": "object",
            "default": {},
        }
    },
    "additionalProperties": False,
}
BAZ_SCHEMA = {
    "description": "Type with nested default value",
    "properties": {
        "a": {"$ref": "#/definitions/Point", "default": {"z": 0.0, "y": 0.0}}
    },
    "type": "object",
    "additionalProperties": False,
}


def test_field_with_default_factory():
    assert Zoo(animal_types={}) == Zoo.from_dict({})
    assert Zoo(
        animal_types={"snake": "reptile", "dog": "mammal"}
    ) == Zoo.from_dict({"animal_types": {"snake": "reptile", "dog": "mammal"}})


def test_field_with_default_dataclass():
    assert Baz(a=Point(0.0, 0.0)) == Baz.from_dict({})


# def test_embeddable_json_schema():
#     expected = {'Point': POINT_SCHEMA, 'Foo': FOO_SCHEMA}
#     assert expected == Foo.json_schema(embeddable=True)
#     expected = {
#         'Point': POINT_SCHEMA,
#         'Foo': FOO_SCHEMA,
#         'Recursive': RECURSIVE_SCHEMA,
#         'Product': PRODUCT_SCHEMA,
#         'ProductList': PRODUCT_LIST_SCHEMA,
#         'Bar': BAR_SCHEMA,
#         'ShoppingCart': SHOPPING_CART_SCHEMA,
#         'OpaqueData': OPAQUE_DATA_SCHEMA,
#         'Zoo': ZOO_SCHEMA,
#         'Baz': BAZ_SCHEMA
#     }
#     assert expected == JsonSchemaMixin.all_json_schemas()
#     with pytest.warns(DeprecationWarning):
#         assert expected == JsonSchemaMixin.json_schema()


def test_json_schema():
    definitions = {"Point": POINT_SCHEMA}
    schema = {
        **FOO_SCHEMA,
        **{
            "definitions": definitions,
            "$schema": "http://json-schema.org/draft-07/schema#",
        },
    }
    assert schema == Foo.json_schema()


def test_serialise_deserialise():
    data = {
        "a": "2018-06-03T12:00:00+00:00",
        "b": [{"z": 1.2, "y": 1.5}],
        "c": {"Mon": 1, "Tue": 2},
        "d": "Wednesday",
        "e": "testing",
        "f": ["xyz", 6],
        "g": ["abc"],
        "h": {"z": 0.5, "y": 1.0},
    }
    f = Foo.from_dict(data)
    assert f.f == ("xyz", 6)
    assert f.g == ("abc",)
    assert data == f.to_dict()


def test_invalid_data():
    with pytest.raises(ValidationError):
        Point.from_dict({"z": 3.14, "y": "wrong"})


def test_newtype_field_validation():
    with pytest.raises(ValidationError):
        Foo.from_dict(
            {
                "a": "2018-06-03T12:00:00+00:00",
                "b": [{"z": 1.2, "y": 1.5}],
                "c": {"Mon": 1, "Tue": 2},
                "d": "Wednesday",
                "e": "t",
                "f": ["xyz", 6],
                "g": ["abc"],
            }
        )


def test_recursive_data():
    data = {"a": "test", "b": {"a": "test2"}}
    r = Recursive.from_dict(data)
    assert r.a == "test"
    assert r.to_dict() == data


def test_recursive_validation():
    # a valid shopping cart containing two items
    data = {
        "items": [
            {"name": "apple", "cost": 0.4},
            {"name": "banana", "cost": 0.6},
        ]
    }
    cart = ShoppingCart.from_dict(data, validate=True)
    assert len(cart.items) == 2
    assert {item.name for item in cart.items} == {"apple", "banana"}
    assert cart.cost == 0.4 + 0.6

    # a shopping cart containing an invalid item
    data = {"items": [{"name": 123}]}
    with pytest.raises(ValidationError):
        ShoppingCart.from_dict(data, validate=True)


def test_non_string_keys():
    p = ProductList(
        products={
            UUID("462b92e8-b3f7-4cb7-ae93-18e829c7e10d"): Product(
                name="hammer", cost=25.10
            )
        }
    )
    expected_data = {
        "products": {
            "462b92e8-b3f7-4cb7-ae93-18e829c7e10d": {
                "name": "hammer",
                "cost": 25.10,
            }
        }
    }
    assert p.to_dict() == expected_data
    assert ProductList.from_dict(expected_data) == p


def test_type_union_schema():
    expected_schema = {
        **BAR_SCHEMA,
        "definitions": {"Point": POINT_SCHEMA},
        "$schema": "http://json-schema.org/draft-07/schema#",
    }
    assert expected_schema == Bar.json_schema()


def test_type_union_serialise():
    assert Bar(a=Weekday.MON).to_dict() == {"a": "Monday"}
    assert Bar(a=Point(x=1.25, y=3.5)).to_dict() == {
        "a": {"z": 1.25, "y": 3.5}
    }


def test_type_union_deserialise():
    assert Bar.from_dict({"a": "Friday"}) == Bar(a=Weekday.FRI)
    assert Bar.from_dict({"a": {"z": 3.6, "y": 10.1}}) == Bar(
        a=Point(x=3.6, y=10.1)
    )


def test_default_values():
    assert Product(name="hammer", cost=20.0) == Product.from_dict(
        {"name": "hammer"}
    )


def test_default_factory():
    @dataclass
    class ClassTest(JsonSchemaMixin):
        attri: List[str] = field(default_factory=lambda: ["val"])

    assert ClassTest().attri == ["val"]
    assert ClassTest().to_dict() == {"attri": ["val"]}
    assert ClassTest.from_dict({}).attri == ["val"]


# def test_read_only_field():
#     @dataclass
#     class Employee(JsonSchemaMixin):
#         name: str
#         department: str
#         id: int = field(metadata=dict(read_only=True), default=-1)

#     schema = Employee.json_schema(embeddable=True)
#     assert schema['Employee']['properties']['id']['readOnly']
#     assert 'readOnly' not in Employee.json_schema()['properties']['id']


# def test_read_only_field_no_default():
#     @dataclass
#     class Employee(JsonSchemaMixin):
#         name: str
#         department: str
#         id: int = field(metadata=dict(read_only=True))

#     with pytest.raises(ValueError):
#         Employee.json_schema(embeddable=True)


def test_optional_field_no_default():
    @dataclass
    class FooBar(JsonSchemaMixin):
        id: Optional[int]

    schema = FooBar.json_schema(embeddable=True)

    assert not hasattr(schema["FooBar"], "required")


def test_required_union_field_no_default():
    @dataclass
    class FooBar(JsonSchemaMixin):
        id: Union[int]

    schema = FooBar.json_schema(embeddable=True)

    assert "id" in schema["FooBar"]["required"]
