import enum
import json
import pickle
import pydoc
import sys
import types

import pytest
import snug

import quiz
from quiz import SELECTOR as _
from quiz import schema as s

from .helpers import MockClient


def trim_whitespace(txt):
    return "".join(t.rstrip() + "\n" for t in txt.splitlines())


def render_doc(obj):
    return trim_whitespace(pydoc.render_doc(obj, renderer=pydoc.plaintext))


@pytest.fixture
def schema(raw_schema):
    return quiz.Schema.from_raw(raw_schema, module="mymodule")


class TestEnumAsType:
    def test_simple(self):
        enum_schema = s.Enum(
            "MyValues",
            "my enum!",
            values=[
                s.EnumValue(
                    "foo", "foo value...", True, "this is deprecated!"
                ),
                s.EnumValue("blabla", "...", False, None),
                s.EnumValue("qux", "qux value.", False, None),
            ],
        )
        created = s.enum_as_type(enum_schema, module="foo")
        assert issubclass(created, quiz.Enum)
        assert issubclass(created, enum.Enum)

        assert created.__name__ == "MyValues"
        assert created.__doc__ == "my enum!"
        assert created.__module__ == "foo"

        assert len(created.__members__) == 3

        for (name, member), member_schema in zip(
            created.__members__.items(), enum_schema.values
        ):
            assert name == member_schema.name
            assert member.name == name
            assert member.value == name
            assert member.__doc__ == member_schema.desc


class TestUnionAsType:
    def test_one(self):
        union_schema = s.Union(
            "Foo", "my union!", [s.TypeRef("BlaType", s.Kind.OBJECT, None)]
        )

        objs = {"BlaType": type("BlaType", (), {})}

        created = s.union_as_type(union_schema, objs)
        assert created.__name__ == "Foo"
        assert created.__doc__ == "my union!"
        assert issubclass(created, quiz.Union)

        assert created.__args__ == (objs["BlaType"],)

    def test_simple(self):
        union_schema = s.Union(
            "Foo",
            "my union!",
            [
                s.TypeRef("BlaType", s.Kind.OBJECT, None),
                s.TypeRef("Quxlike", s.Kind.INTERFACE, None),
                s.TypeRef("Foobar", s.Kind.UNION, None),
            ],
        )

        objs = {
            "BlaType": type("BlaType", (), {}),
            "Quxlike": type("Quxlike", (), {}),
            "Foobar": type("Foobar", (), {}),
            "Bla": type("Bla", (), {}),
        }

        created = s.union_as_type(union_schema, objs)
        assert created.__name__ == "Foo"
        assert created.__doc__ == "my union!"
        assert issubclass(created, quiz.Union)

        assert created.__args__ == (
            objs["BlaType"],
            objs["Quxlike"],
            objs["Foobar"],
        )


class TestInterfaceAsType:
    def test_simple(self):
        interface_schema = s.Interface(
            "Foo",
            "my interface!",
            [
                s.Field(
                    "blabla",
                    type=s.TypeRef("String", s.Kind.SCALAR, None),
                    args=[],
                    desc="my description",
                    is_deprecated=False,
                    deprecation_reason=None,
                )
            ],
        )
        created = s.interface_as_type(interface_schema, module="mymodule")

        assert isinstance(created, quiz.Interface)
        assert issubclass(created, quiz.types.Namespace)
        assert created.__name__ == "Foo"
        assert created.__doc__ == "my interface!"
        assert created.__module__ == "mymodule"


class TestObjectAsType:
    def test_simple(self):
        obj_schema = s.Object(
            "Foo",
            "the foo description!",
            interfaces=[
                s.TypeRef("Interface1", s.Kind.INTERFACE, None),
                s.TypeRef("BlaInterface", s.Kind.INTERFACE, None),
            ],
            input_fields=None,
            fields=[
                s.Field(
                    "blabla",
                    type=s.TypeRef("String", s.Kind.SCALAR, None),
                    args=[],
                    desc="my description",
                    is_deprecated=False,
                    deprecation_reason=None,
                )
            ],
        )
        interfaces = {
            "Interface1": type(
                "Interface1", (quiz.Interface,), {"__module__": "foo"}
            ),
            "BlaInterface": type(
                "BlaInterface", (quiz.Interface,), {"__module__": "foo"}
            ),
            "Qux": type("Qux", (quiz.Interface,), {"__module__": "foo"}),
        }
        created = s.object_as_type(obj_schema, interfaces, module="foo")
        assert issubclass(created, quiz.Object)
        assert created.__name__ == "Foo"
        assert created.__doc__ == "the foo description!"
        assert created.__module__ == "foo"
        assert issubclass(created, interfaces["Interface1"])
        assert issubclass(created, interfaces["BlaInterface"])


class TestResolveTypeRef:
    def test_default(self):
        ref = s.TypeRef("Foo", s.Kind.ENUM, None)

        classes = {"Foo": quiz.Enum("Foo", {})}
        resolved = s.resolve_typeref(ref, classes)
        assert issubclass(resolved, quiz.Nullable)
        assert resolved.__arg__ is classes["Foo"]

    def test_non_null(self):
        ref = s.TypeRef(
            None, s.Kind.NON_NULL, s.TypeRef("Foo", s.Kind.OBJECT, None)
        )

        classes = {"Foo": type("Foo", (), {})}
        resolved = s.resolve_typeref(ref, classes)
        assert resolved == classes["Foo"]

    def test_list(self):
        ref = s.TypeRef(
            None, s.Kind.LIST, s.TypeRef("Foo", s.Kind.OBJECT, None)
        )
        classes = {"Foo": type("Foo", (), {})}
        resolved = s.resolve_typeref(ref, classes)
        assert issubclass(resolved, quiz.Nullable)
        assert issubclass(resolved.__arg__, quiz.List)
        assert issubclass(resolved.__arg__.__arg__, quiz.Nullable)
        assert resolved.__arg__.__arg__.__arg__ == classes["Foo"]

    def test_list_non_null(self):
        ref = s.TypeRef(
            None,
            s.Kind.NON_NULL,
            s.TypeRef(
                None,
                s.Kind.LIST,
                s.TypeRef(
                    None,
                    s.Kind.NON_NULL,
                    s.TypeRef("Foo", s.Kind.OBJECT, None),
                ),
            ),
        )
        classes = {"Foo": type("Foo", (), {})}
        resolved = s.resolve_typeref(ref, classes)
        assert issubclass(resolved, quiz.List)
        assert resolved.__arg__ == classes["Foo"]


class TestSchemaFromRaw:
    def test_scalars(self, raw_schema):
        class URI(quiz.Scalar):
            pass

        schema = quiz.Schema.from_raw(raw_schema, scalars=[URI], module="foo")

        # generic scalars
        assert issubclass(schema.DateTime, quiz.GenericScalar)
        assert schema.DateTime.__name__ == "DateTime"
        assert len(schema.__doc__) > 0

        assert schema.Boolean is bool
        assert schema.String is str
        assert schema.Float is float
        assert schema.Int is int

        assert schema.URI is URI

    def test_defaults(self, raw_schema):
        schema = quiz.Schema.from_raw(raw_schema, module="foo")
        assert isinstance(schema, quiz.Schema)
        assert issubclass(schema.DateTime, quiz.GenericScalar)
        assert schema.String is str
        assert "Query" in schema.classes
        assert schema.query_type == schema.classes["Query"]
        assert schema.mutation_type == schema.classes["Mutation"]
        assert schema.subscription_type is None
        assert schema.raw == raw_schema


class TestSchema:
    def test_attributes(self, schema):
        assert schema.Query is schema.classes["Query"]
        assert schema.module == "mymodule"
        assert issubclass(schema.classes["Repository"], quiz.Object)
        assert "Repository" in dir(schema)
        assert "__class__" in dir(schema)

        with pytest.raises(AttributeError, match="foo"):
            schema.foo

    def test_populate_module(self, raw_schema, mocker):
        mymodule = types.ModuleType("mymodule")
        mocker.patch.dict(sys.modules, {"mymodule": mymodule})

        schema = quiz.Schema.from_raw(raw_schema, module="mymodule")

        with pytest.raises(AttributeError, match="Repository"):
            mymodule.Repository

        schema.populate_module()

        assert mymodule.Repository is schema.Repository

        my_obj = mymodule.Repository(description="...", name="my repo")
        loaded = pickle.loads(pickle.dumps(my_obj))
        assert loaded == my_obj

    def test_populate_module_no_module(self, raw_schema):
        schema = quiz.Schema.from_raw(raw_schema)
        assert schema.module is None

        with pytest.raises(RuntimeError, match="module"):
            schema.populate_module()

    def test_query(self, schema):

        query = schema.query[_.license(key="MIT")]
        assert query == quiz.Query(
            cls=schema.Query,
            selections=quiz.SelectionSet(
                quiz.Field("license", {"key": "MIT"})
            ),
        )
        with pytest.raises(quiz.SelectionError):
            schema.query[_.foo]

    def test_to_path(self, schema, tmpdir):

        path = str(tmpdir / "myschema.json")

        class MyPath(object):
            def __fspath__(self):
                return path

        schema.to_path(MyPath())
        loaded = schema.from_path(MyPath(), module="mymodule")
        assert loaded.classes.keys() == schema.classes.keys()


class TestSchemaFromUrl:
    def test_success(self, raw_schema):
        client = MockClient(
            snug.Response(
                200, json.dumps({"data": {"__schema": raw_schema}}).encode()
            )
        )
        result = quiz.Schema.from_url("https://my.url/graphql", client=client)

        assert client.request.url == "https://my.url/graphql"
        assert isinstance(result, quiz.Schema)
        assert result.raw == raw_schema

    def test_fails(self, raw_schema):
        client = MockClient(
            snug.Response(
                200,
                json.dumps(
                    {"data": {"__schema": None}, "errors": "foo"}
                ).encode(),
            )
        )

        with pytest.raises(quiz.ErrorResponse):
            quiz.Schema.from_url("https://my.url/graphql", client=client)

    @pytest.mark.live
    def test_live(self):  # pragma: no cover
        schema = quiz.Schema.from_url("https://graphql-pokemon.now.sh/")
        assert schema.Pokemon


class TestSchemaFromPath:
    def test_defaults(self, raw_schema, tmpdir):
        schema_file = tmpdir / "myfile.json"
        with schema_file.open("w") as wfile:
            json.dump(raw_schema, wfile)

        schema = quiz.Schema.from_path(schema_file)
        assert schema.module is None

    def test_success(self, raw_schema, tmpdir):

        schema_file = tmpdir / "myfile.json"
        with schema_file.open("w") as wfile:
            json.dump(raw_schema, wfile)

        class MyPath(object):
            def __fspath__(self):
                return str(schema_file)

        schema = quiz.Schema.from_path(MyPath(), module="mymodule")
        assert isinstance(schema, quiz.Schema)

    def test_does_not_exist(self, tmpdir):
        schema_file = tmpdir / "does-not-exist.json"

        with pytest.raises(IOError):
            quiz.Schema.from_path(str(schema_file), module="mymodule")


def test_end_to_end(raw_schema):
    schema = quiz.Schema.from_raw(raw_schema, module="github")
    doc = render_doc(schema.Issue)

    assert (
        """\
 |  viewerDidAuthor
 |      : bool
 |      Did the viewer author this comment."""
        in doc
    )
    assert (
        """\
 |  publishedAt
 |      : DateTime or None
 |      Identifies when the comment was published at."""
        in doc
    )

    assert (
        """\
 |  viewerCannotUpdateReasons
 |      : [CommentCannotUpdateReason]
 |      Reasons why the current viewer can not update this comment."""
        in doc
    )

    assert schema.Issue.__doc__ in doc
    assert "Labelable" in doc