import enum

import pytest
from sqlalchemy import Column, func, select, types
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.inspection import inspect
from sqlalchemy.orm import column_property, composite

try:
    from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType
except ImportError:
    ChoiceType = JSONType = ScalarListType = TSVectorType = object

import graphene
from graphene.relay import Node
from graphene.types.datetime import DateTime
from graphene.types.json import JSONString

from alchql.converter import (
    convert_sqlalchemy_column,
    convert_sqlalchemy_composite,
    convert_sqlalchemy_relationship,
)
from alchql.fields import (
    UnsortedSQLAlchemyConnectionField,
    default_connection_field_factory,
)
from alchql.registry import Registry, get_global_registry
from alchql.types import SQLAlchemyObjectType
from .models import Article, CompositeFullName, Pet, Reporter


def mock_resolver():
    """Empty resolver"""


def get_field(sqlalchemy_type, **column_kwargs):
    class Model(declarative_base()):
        __tablename__ = "model"
        id_ = Column(types.Integer, primary_key=True)
        column = Column(sqlalchemy_type, doc="Custom Help Text", **column_kwargs)

    column_prop = inspect(Model).column_attrs["column"]
    return convert_sqlalchemy_column(column_prop, get_global_registry(), mock_resolver)


def get_field_from_column(column_):
    class Model(declarative_base()):
        __tablename__ = "model"
        id_ = Column(types.Integer, primary_key=True)
        column = column_

    column_prop = inspect(Model).column_attrs["column"]
    return convert_sqlalchemy_column(column_prop, get_global_registry(), mock_resolver)


def test_should_unknown_sqlalchemy_field_raise_exception():
    re_err = "Don't know how to convert the SQLAlchemy field"

    # support legacy Binary type and subsequent LargeBinary

    LargeBinary = getattr(types, "LargeBinary")

    with pytest.raises(Exception, match=re_err):
        get_field(LargeBinary())


def test_should_date_convert_string():
    assert get_field(types.Date()).type == graphene.Date


def test_should_datetime_convert_datetime():
    assert get_field(types.DateTime()).type == DateTime


def test_should_time_convert_string():
    assert get_field(types.Time()).type == graphene.String


def test_should_string_convert_string():
    assert get_field(types.String()).type == graphene.String


def test_should_text_convert_string():
    assert get_field(types.Text()).type == graphene.String


def test_should_unicode_convert_string():
    assert get_field(types.Unicode()).type == graphene.String


def test_should_unicodetext_convert_string():
    assert get_field(types.UnicodeText()).type == graphene.String


def test_should_enum_convert_enum():
    field = get_field(types.Enum(enum.Enum("TwoNumbers", ("one", "two"))))
    field_type = field.type()
    assert isinstance(field_type, graphene.Enum)
    assert field_type._meta.name == "TwoNumbers"
    assert hasattr(field_type, "ONE")
    assert not hasattr(field_type, "one")
    assert hasattr(field_type, "TWO")
    assert not hasattr(field_type, "two")

    field = get_field(types.Enum("one", "two", name="two_numbers"))
    field_type = field.type()
    assert isinstance(field_type, graphene.Enum)
    assert field_type._meta.name == "TwoNumbers"
    assert hasattr(field_type, "ONE")
    assert not hasattr(field_type, "one")
    assert hasattr(field_type, "TWO")
    assert not hasattr(field_type, "two")


def test_should_not_enum_convert_enum_without_name():
    field = get_field(types.Enum("one", "two"))
    re_err = r"No type name specified for Enum\('one', 'two'\)"
    with pytest.raises(TypeError, match=re_err):
        field.type()


def test_should_small_integer_convert_int():
    assert get_field(types.SmallInteger()).type == graphene.Int


def test_should_big_integer_convert_float():
    assert get_field(types.BigInteger()).type == graphene.Float


def test_should_integer_convert_int():
    assert get_field(types.Integer()).type == graphene.Int


def test_should_primary_integer_convert_id():
    assert get_field(types.Integer(), primary_key=True).type == graphene.NonNull(
        graphene.ID
    )


def test_should_boolean_convert_boolean():
    assert get_field(types.Boolean()).type == graphene.Boolean


def test_should_float_convert_float():
    assert get_field(types.Float()).type == graphene.Float


def test_should_numeric_convert_float():
    assert get_field(types.Numeric()).type == graphene.Float


def test_should_choice_convert_enum():
    field = get_field(ChoiceType([("es", "Spanish"), ("en", "English")]))
    graphene_type = field.type
    assert issubclass(graphene_type, graphene.Enum)
    assert graphene_type._meta.name == "MODEL_COLUMN"
    assert graphene_type._meta.enum.__members__["es"].value == "Spanish"
    assert graphene_type._meta.enum.__members__["en"].value == "English"


def test_should_enum_choice_convert_enum():
    class TestEnum(enum.Enum):
        es = "Spanish"
        en = "English"

    field = get_field(ChoiceType(TestEnum, impl=types.String()))
    graphene_type = field.type
    assert issubclass(graphene_type, graphene.Enum)
    assert graphene_type._meta.name == "MODEL_COLUMN"
    assert graphene_type._meta.enum.__members__["es"].value == "Spanish"
    assert graphene_type._meta.enum.__members__["en"].value == "English"


def test_should_intenum_choice_convert_enum():
    class TestEnum(enum.IntEnum):
        one = 1
        two = 2

    field = get_field(ChoiceType(TestEnum, impl=types.String()))
    graphene_type = field.type
    assert issubclass(graphene_type, graphene.Enum)
    assert graphene_type._meta.name == "MODEL_COLUMN"
    assert graphene_type._meta.enum.__members__["one"].value == 1
    assert graphene_type._meta.enum.__members__["two"].value == 2


def test_should_columproperty_convert():
    field = get_field_from_column(
        column_property(select([func.sum(func.cast(id, types.Integer))]).where(id == 1))
    )

    assert field.type == graphene.Int


def test_should_scalar_list_convert_list():
    field = get_field(ScalarListType())
    assert isinstance(field.type, graphene.List)
    assert field.type.of_type == graphene.String


def test_should_jsontype_convert_jsonstring():
    assert get_field(JSONType()).type == JSONString


def test_should_tsvector_convert_string():
    assert get_field(TSVectorType).type == graphene.String


def test_should_tsvector_convert_string2():
    assert get_field(postgresql.TSVECTOR()).type == graphene.String


def test_should_manytomany_convert_connectionorlist():
    class A(SQLAlchemyObjectType):
        class Meta:
            model = Article

    dynamic_field = convert_sqlalchemy_relationship(
        Reporter.pets.property,
        A,
        default_connection_field_factory,
        "orm_field_name",
    )
    assert isinstance(dynamic_field, graphene.Dynamic)
    assert not dynamic_field.get_type()


def test_should_manytomany_convert_connectionorlist_list():
    class A(SQLAlchemyObjectType):
        class Meta:
            model = Pet

    dynamic_field = convert_sqlalchemy_relationship(
        Reporter.pets.property,
        A,
        default_connection_field_factory,
        "orm_field_name",
    )
    assert isinstance(dynamic_field, graphene.Dynamic)
    graphene_type = dynamic_field.get_type()
    assert isinstance(graphene_type, graphene.Field)
    assert isinstance(graphene_type.type, graphene.List)
    assert graphene_type.type.of_type == A


def test_should_manytomany_convert_connectionorlist_connection():
    class A(SQLAlchemyObjectType):
        class Meta:
            model = Pet
            interfaces = (Node,)

    dynamic_field = convert_sqlalchemy_relationship(
        Reporter.pets.property,
        A,
        default_connection_field_factory,
        "orm_field_name",
    )
    assert isinstance(dynamic_field, graphene.Dynamic)
    assert isinstance(dynamic_field.get_type(), UnsortedSQLAlchemyConnectionField)


def test_should_manytoone_convert_connectionorlist():
    class A(SQLAlchemyObjectType):
        class Meta:
            model = Article

    dynamic_field = convert_sqlalchemy_relationship(
        Reporter.pets.property,
        A,
        default_connection_field_factory,
        "orm_field_name",
    )
    assert isinstance(dynamic_field, graphene.Dynamic)
    assert not dynamic_field.get_type()


def test_should_manytoone_convert_connectionorlist_list():
    class A(SQLAlchemyObjectType):
        class Meta:
            model = Article

    dynamic_field = convert_sqlalchemy_relationship(
        Reporter.favorite_article.property,
        A,
        default_connection_field_factory,
        "orm_field_name",
    )
    assert isinstance(dynamic_field, graphene.Dynamic)
    graphene_type = dynamic_field.get_type()
    assert isinstance(graphene_type, graphene.Field)
    assert graphene_type.type == A


def test_should_manytoone_convert_connectionorlist_connection():
    class A(SQLAlchemyObjectType):
        class Meta:
            model = Article
            interfaces = (Node,)

    dynamic_field = convert_sqlalchemy_relationship(
        Reporter.favorite_article.property,
        A,
        default_connection_field_factory,
        "orm_field_name",
    )
    assert isinstance(dynamic_field, graphene.Dynamic)
    graphene_type = dynamic_field.get_type()
    assert isinstance(graphene_type, graphene.Field)
    assert graphene_type.type == A


def test_should_onetoone_convert_field():
    class A(SQLAlchemyObjectType):
        class Meta:
            model = Article
            interfaces = (Node,)

    dynamic_field = convert_sqlalchemy_relationship(
        Reporter.favorite_article.property,
        A,
        default_connection_field_factory,
        "orm_field_name",
    )
    assert isinstance(dynamic_field, graphene.Dynamic)
    graphene_type = dynamic_field.get_type()
    assert isinstance(graphene_type, graphene.Field)
    assert graphene_type.type == A


def test_should_postgresql_uuid_convert():
    assert get_field(postgresql.UUID()).type == graphene.String


def test_should_postgresql_enum_convert():
    field = get_field(postgresql.ENUM("one", "two", name="two_numbers"))
    field_type = field.type()
    assert isinstance(field_type, graphene.Enum)
    assert field_type._meta.name == "TwoNumbers"
    assert hasattr(field_type, "ONE")
    assert not hasattr(field_type, "one")
    assert hasattr(field_type, "TWO")
    assert not hasattr(field_type, "two")


def test_should_postgresql_py_enum_convert():
    field = get_field(
        postgresql.ENUM(enum.Enum("TwoNumbers", "one two"), name="two_numbers")
    )
    field_type = field.type()
    assert field_type._meta.name == "TwoNumbers"
    assert isinstance(field_type, graphene.Enum)
    assert hasattr(field_type, "ONE")
    assert not hasattr(field_type, "one")
    assert hasattr(field_type, "TWO")
    assert not hasattr(field_type, "two")


def test_should_postgresql_array_convert():
    field = get_field(postgresql.ARRAY(types.Integer))
    assert isinstance(field.type, graphene.List)
    assert field.type.of_type == graphene.Int


def test_should_array_convert():
    field = get_field(types.ARRAY(types.Integer))
    assert isinstance(field.type, graphene.List)
    assert field.type.of_type == graphene.Int


def test_should_postgresql_json_convert():
    assert get_field(postgresql.JSON()).type == graphene.JSONString


def test_should_postgresql_jsonb_convert():
    assert get_field(postgresql.JSONB()).type == graphene.JSONString


def test_should_postgresql_hstore_convert():
    assert get_field(postgresql.HSTORE()).type == graphene.JSONString


def test_should_composite_convert():
    registry = Registry()

    class CompositeClass:
        def __init__(self, col1, col2):
            self.col1 = col1
            self.col2 = col2

    @convert_sqlalchemy_composite.register(CompositeClass, registry)
    def convert_composite_class(composite, registry):
        return graphene.String(description=composite.doc)

    field = convert_sqlalchemy_composite(
        composite(
            CompositeClass,
            (Column(types.Unicode(50)), Column(types.Unicode(50))),
            doc="Custom Help Text",
        ),
        registry,
        mock_resolver,
    )
    assert isinstance(field, graphene.String)


def test_should_unknown_sqlalchemy_composite_raise_exception():
    class CompositeClass:
        def __init__(self, col1, col2):
            self.col1 = col1
            self.col2 = col2

    re_err = "Don't know how to convert the composite field"
    with pytest.raises(Exception, match=re_err):
        convert_sqlalchemy_composite(
            composite(
                CompositeFullName,
                (Column(types.Unicode(50)), Column(types.Unicode(50))),
            ),
            Registry(),
            mock_resolver,
        )
