from datetime import datetime, timezone
from typing import List

import pytest
from pydantic_sqlalchemy import sqlalchemy_to_pydantic
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy_utc import UtcDateTime


@pytest.fixture
def User(Base) -> "User":
    class User(Base):
        __tablename__ = "users"

        id = Column(Integer, primary_key=True)
        name = Column(String)
        fullname = Column(String)
        nickname = Column(String)
        created = Column(DateTime, default=datetime.utcnow)
        updated = Column(UtcDateTime, default=utc_now, onupdate=utc_now)

        addresses = relationship(
            "Address", back_populates="user", cascade="all, delete, delete-orphan"
        )

    return User


@pytest.fixture
def Address(Base) -> "Address":
    class Address(Base):
        __tablename__ = "addresses"

        id = Column(Integer, primary_key=True)
        email_address = Column(String, nullable=False)
        user_id = Column(Integer, ForeignKey("users.id"))

        user = relationship("User", back_populates="addresses")

    return Address


@pytest.fixture
def default_user_with_addresses(Address, User):
    ed_user = User(name="ed", fullname="Ed Jones", nickname="edsnickname")
    address = Address(email_address="ed@example.com")
    address2 = Address(email_address="eddy@example.com")
    ed_user.addresses = [address, address2]

    return ed_user


def utc_now() -> datetime:
    return datetime.now(tz=timezone.utc)


def test_defaults(User, Address, default_user_with_addresses, create_db) -> None:
    db = create_db([default_user_with_addresses])

    PydanticUser = sqlalchemy_to_pydantic(User)
    PydanticAddress = sqlalchemy_to_pydantic(Address)

    class PydanticUserWithAddresses(PydanticUser):
        addresses: List[PydanticAddress] = []

    user = db.query(User).first()
    pydantic_user = PydanticUser.from_orm(user)
    data = pydantic_user.dict()
    assert isinstance(data["created"], datetime)
    assert isinstance(data["updated"], datetime)
    check_data = data.copy()
    del check_data["created"]
    del check_data["updated"]
    assert check_data == {
        "fullname": "Ed Jones",
        "id": 1,
        "name": "ed",
        "nickname": "edsnickname",
    }
    pydantic_user_with_addresses = PydanticUserWithAddresses.from_orm(user)
    data = pydantic_user_with_addresses.dict()
    assert isinstance(data["updated"], datetime)
    assert isinstance(data["created"], datetime)
    check_data = data.copy()
    del check_data["updated"]
    del check_data["created"]
    assert check_data == {
        "fullname": "Ed Jones",
        "id": 1,
        "name": "ed",
        "nickname": "edsnickname",
        "addresses": [
            {"email_address": "ed@example.com", "id": 1, "user_id": 1},
            {"email_address": "eddy@example.com", "id": 2, "user_id": 1},
        ],
    }


def test_schema(User, Address) -> None:
    PydanticUser = sqlalchemy_to_pydantic(User)
    PydanticAddress = sqlalchemy_to_pydantic(Address)
    assert PydanticUser.schema() == {
        "title": "User",
        "type": "object",
        "properties": {
            "id": {"title": "Id", "type": "integer"},
            "name": {"title": "Name", "type": "string"},
            "fullname": {"title": "Fullname", "type": "string"},
            "nickname": {"title": "Nickname", "type": "string"},
            "created": {"title": "Created", "type": "string", "format": "date-time"},
            "updated": {"title": "Updated", "type": "string", "format": "date-time"},
        },
        "required": ["id"],
    }
    assert PydanticAddress.schema() == {
        "title": "Address",
        "type": "object",
        "properties": {
            "id": {"title": "Id", "type": "integer"},
            "email_address": {"title": "Email Address", "type": "string"},
            "user_id": {"title": "User Id", "type": "integer"},
        },
        "required": ["id", "email_address"],
    }


def test_config(User, Address, default_user_with_addresses, create_db) -> None:
    db = create_db([default_user_with_addresses])

    class Config:
        orm_mode = True
        allow_population_by_field_name = True

        @classmethod
        def alias_generator(cls, string: str) -> str:
            pascal_case = "".join(word.capitalize() for word in string.split("_"))
            camel_case = pascal_case[0].lower() + pascal_case[1:]
            return camel_case

    PydanticUser = sqlalchemy_to_pydantic(User)
    PydanticAddress = sqlalchemy_to_pydantic(Address, config=Config)

    class PydanticUserWithAddresses(PydanticUser):
        addresses: List[PydanticAddress] = []

    user = db.query(User).first()
    pydantic_user_with_addresses = PydanticUserWithAddresses.from_orm(user)
    data = pydantic_user_with_addresses.dict(by_alias=True)
    assert isinstance(data["created"], datetime)
    assert isinstance(data["updated"], datetime)
    check_data = data.copy()
    del check_data["created"]
    del check_data["updated"]
    assert check_data == {
        "fullname": "Ed Jones",
        "id": 1,
        "name": "ed",
        "nickname": "edsnickname",
        "addresses": [
            {"emailAddress": "ed@example.com", "id": 1, "userId": 1},
            {"emailAddress": "eddy@example.com", "id": 2, "userId": 1},
        ],
    }


def test_exclude(User, Address, default_user_with_addresses, create_db) -> None:
    db = create_db([default_user_with_addresses])

    PydanticUser = sqlalchemy_to_pydantic(User, exclude={"nickname"})
    PydanticAddress = sqlalchemy_to_pydantic(Address, exclude={"user_id"})

    class PydanticUserWithAddresses(PydanticUser):
        addresses: List[PydanticAddress] = []

    user = db.query(User).first()
    pydantic_user_with_addresses = PydanticUserWithAddresses.from_orm(user)
    data = pydantic_user_with_addresses.dict(by_alias=True)
    assert isinstance(data["created"], datetime)
    assert isinstance(data["updated"], datetime)
    check_data = data.copy()
    del check_data["created"]
    del check_data["updated"]
    assert check_data == {
        "fullname": "Ed Jones",
        "id": 1,
        "name": "ed",
        "addresses": [
            {"email_address": "ed@example.com", "id": 1},
            {"email_address": "eddy@example.com", "id": 2},
        ],
    }


def test_model_with_property(Base) -> None:
    class Person(Base):
        __tablename__ = "persons"
        id = Column(Integer, primary_key=True)
        name = Column(String)
        firstname = Column(String)

        @property
        def full_name(self) -> str:
            return self.firstname + " " + self.name

    PydanticPerson = sqlalchemy_to_pydantic(Person)

    person = Person(id=1, name="Doe", firstname="John")
    pydantic_person = PydanticPerson.from_orm(person)
    data = pydantic_person.dict()

    assert data["id"] == 1
    assert data["name"] == "Doe"
    assert data["firstname"] == "John"
    assert data["full_name"] == "John Doe"


def test_model_with_hybrid_property(Base) -> None:
    class Person(Base):
        __tablename__ = "persons"
        id = Column(Integer, primary_key=True)
        name = Column(String)
        firstname = Column(String)

        @hybrid_property
        def full_name(self) -> str:
            return self.firstname + " " + self.name

    PydanticPerson = sqlalchemy_to_pydantic(Person)

    person = Person(id=1, name="Doe", firstname="John")
    pydantic_person = PydanticPerson.from_orm(person)
    data = pydantic_person.dict()

    assert data["id"] == 1
    assert data["name"] == "Doe"
    assert data["firstname"] == "John"
    assert data["full_name"] == "John Doe"


def test_model_with_hybrid_property_exclude(Base) -> None:
    class Person(Base):
        __tablename__ = "persons"
        id = Column(Integer, primary_key=True)
        name = Column(String)
        firstname = Column(String)

        @hybrid_property
        def full_name(self) -> str:
            return self.firstname + " " + self.name

    PydanticPerson = sqlalchemy_to_pydantic(Person, exclude=["full_name"])

    person = Person(id=1, name="Doe", firstname="John")
    pydantic_person = PydanticPerson.from_orm(person)
    data = pydantic_person.dict()

    assert data["id"] == 1
    assert data["name"] == "Doe"
    assert data["firstname"] == "John"
    assert "full_name" not in data
