import textwrap

MYPY = False
if MYPY:
    import typing  # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression

import unittest
try:
    # Works for Py 3.3+
    from unittest.mock import Mock
except ImportError:
    # See https://github.com/python/mypy/issues/1153#issuecomment-253842414
    from unittest.mock import Mock  # type: ignore

from stone.ir import (
    Alias,
    Api,
    ApiNamespace,
    Boolean,
    List,
    Map,
    Nullable,
    String,
    Struct,
    StructField,
    Timestamp,
    UInt64,
    Union,
    UnionField,
    Void,
    Float64)
from stone.ir.api import ApiRoute
from stone.backends.python_type_stubs import PythonTypeStubsBackend


ISO_8601_FORMAT = "%Y-%m-%dT%H:%M:%SZ"

def _make_backend():
    # type: () -> PythonTypeStubsBackend
    return PythonTypeStubsBackend(
        target_folder_path=Mock(),
        args=['--package', 'output'],
    )

def _make_namespace_with_alias():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace('ns_with_alias')

    struct1 = Struct(name='Struct1', namespace=ns, ast_node=None)
    struct1.set_attributes(None, [StructField('f1', Boolean(), None, None)])
    ns.add_data_type(struct1)

    alias = Alias(name='AliasToStruct1', namespace=ns, ast_node=None)
    alias.set_attributes(doc=None, data_type=struct1)
    ns.add_alias(alias)

    str_type = String(min_length=3)
    str_alias = Alias(name='NotUserDefinedAlias', namespace=ns, ast_node=None)
    str_alias.set_attributes(doc=None, data_type=str_type)
    ns.add_alias(str_alias)

    return ns

def _make_namespace_with_many_structs():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace('ns_with_many_structs')

    struct1 = Struct(name='Struct1', namespace=ns, ast_node=None)
    struct1.set_attributes(None, [StructField('f1', Boolean(), None, None)])
    ns.add_data_type(struct1)

    struct2 = Struct(name='Struct2', namespace=ns, ast_node=None)
    struct2.set_attributes(
        doc=None,
        fields=[
            StructField('f2', List(UInt64()), None, None),
            StructField('f3', Timestamp(ISO_8601_FORMAT), None, None),
            StructField('f4', Map(String(), UInt64()), None, None)
        ]
    )
    ns.add_data_type(struct2)

    return ns

def _make_namespace_with_nested_types():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace('ns_w_nested_types')

    struct = Struct(name='NestedTypes', namespace=ns, ast_node=None)
    struct.set_attributes(
        doc=None,
        fields=[
            StructField(
                name='NullableList',
                data_type=Nullable(
                    List(UInt64())
                ),
                doc=None,
                ast_node=None,
            ),
            StructField(
                name='ListOfNullables',
                data_type=List(
                    Nullable(UInt64())
                ),
                doc=None,
                ast_node=None,
            )
        ]
    )
    ns.add_data_type(struct)

    return ns

def _make_namespace_with_a_union():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace('ns_with_a_union')

    u1 = Union(name='Union', namespace=ns, ast_node=None, closed=True)
    u1.set_attributes(
        doc=None,
        fields=[
            UnionField(
                name="first",
                doc=None,
                data_type=Void(),
                ast_node=None
            ),
            UnionField(
                name="last",
                doc=None,
                data_type=Void(),
                ast_node=None
            ),
        ],
    )
    ns.add_data_type(u1)

    # A more interesting case with non-void variants.
    shape_union = Union(name='Shape', namespace=ns, ast_node=None, closed=True)
    shape_union.set_attributes(
        doc=None,
        fields=[
            UnionField(
                name="point",
                doc=None,
                data_type=Void(),
                ast_node=None
            ),
            UnionField(
                name="circle",
                doc=None,
                data_type=Float64(),
                ast_node=None
            ),
        ],
    )
    ns.add_data_type(shape_union)

    return ns

def _make_namespace_with_empty_union():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace('ns_with_empty_union')

    union = Union(name='EmptyUnion', namespace=ns, ast_node=None, closed=True)
    union.set_attributes(
        doc=None,
        fields=[],
    )
    ns.add_data_type(union)

    return ns

def _make_namespace_with_route():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace("_make_namespace_with_route()")
    mock_ast_node = Mock()
    route_one = ApiRoute(
        name="route_one",
        version=1,
        ast_node=mock_ast_node,
    )
    route_two = ApiRoute(
        name="route_one",
        version=2,
        ast_node=mock_ast_node,
    )
    ns.add_route(route_one)
    ns.add_route(route_two)
    return ns

def _make_namespace_with_route_name_conflict():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace("_make_namespace_with_route()")
    mock_ast_node = Mock()
    route_one = ApiRoute(
        name="route_one_v2",
        version=1,
        ast_node=mock_ast_node,
    )
    route_two = ApiRoute(
        name="route_one",
        version=2,
        ast_node=mock_ast_node,
    )
    ns.add_route(route_one)
    ns.add_route(route_two)
    return ns

def _make_namespace_with_nullable_and_dafault_fields():
    # type: (...) -> ApiNamespace
    ns = ApiNamespace('ns_w_nullable__fields')

    struct = Struct(name='Struct1', namespace=ns, ast_node=None)
    default_field = StructField(
                        name='DefaultField',
                        data_type=UInt64(),
                        doc=None,
                        ast_node=None,
                    )
    default_field.set_default(1)
    struct.set_attributes(
        doc=None,
        fields=[
            StructField(
                name='NullableField',
                data_type=Nullable(
                    UInt64()
                ),
                doc=None,
                ast_node=None,
            ),
            default_field,
            StructField(
                name='RequiredField',
                data_type=UInt64(),
                doc=None,
                ast_node=None,
            )
        ]
    )
    ns.add_data_type(struct)

    return ns

def _api():
    api = Api(version="1.0")
    return api

_headers = """\
# -*- coding: utf-8 -*-
# Auto-generated by Stone, do not modify.
# @generated
# flake8: noqa
# pylint: skip-file

{}
from stone.backends.python_rsrc import stone_base as bb  # type: ignore
from stone.backends.python_rsrc import stone_validators as bv  # type: ignore

T = TypeVar('T', bound=bb.AnnotationType)
U = TypeVar('U')"""

class TestPythonTypeStubs(unittest.TestCase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.maxDiff = 1000000  # Increase text diff size

    def _evaluate_namespace(self, ns):
        # type: (ApiNamespace) -> typing.Text
        backend = _make_backend()
        backend._generate_base_namespace_module(ns)
        return backend.output_buffer_to_string()

    def test__generate_base_namespace_module__with_many_structs(self):
        # type: () -> None
        ns = _make_namespace_with_many_structs()
        result = self._evaluate_namespace(ns)
        expected = textwrap.dedent("""\
            {headers}

            class Struct1(bb.Struct):
                def __init__(self,
                             f1: bool = ...) -> None: ...
                f1: bb.Attribute[bool] = ...
                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            Struct1_validator: bv.Validator = ...

            class Struct2(bb.Struct):
                def __init__(self,
                             f2: List[int] = ...,
                             f3: datetime.datetime = ...,
                             f4: Dict[Text, int] = ...) -> None: ...
                f2: bb.Attribute[List[int]] = ...
                f3: bb.Attribute[datetime.datetime] = ...
                f4: bb.Attribute[Dict[Text, int]] = ...
                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            Struct2_validator: bv.Validator = ...

            """).format(headers=_headers.format(textwrap.dedent("""\
                from typing import (
                    Callable,
                    Dict,
                    List,
                    Text,
                    Type,
                    TypeVar,
                )

                import datetime""")))
        self.assertEqual(result, expected)

    def test__generate_base_namespace_module__with_nested_types(self):
        # type: () -> None
        ns = _make_namespace_with_nested_types()
        result = self._evaluate_namespace(ns)
        expected = textwrap.dedent("""\
            {headers}

            class NestedTypes(bb.Struct):
                def __init__(self,
                             list_of_nullables: List[Optional[int]] = ...,
                             nullable_list: Optional[List[int]] = ...) -> None: ...
                list_of_nullables: bb.Attribute[List[Optional[int]]] = ...
                nullable_list: bb.Attribute[Optional[List[int]]] = ...
                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            NestedTypes_validator: bv.Validator = ...

            """).format(headers=_headers.format(textwrap.dedent("""\
                from typing import (
                    Callable,
                    List,
                    Optional,
                    Text,
                    Type,
                    TypeVar,
                )""")))
        self.assertEqual(result, expected)

    def test__generate_base_namespace_module_with_union__generates_stuff(self):
        # type: () -> None
        ns = _make_namespace_with_a_union()
        result = self._evaluate_namespace(ns)
        expected = textwrap.dedent("""\
            {headers}

            class Union(bb.Union):
                first: Union = ...
                last: Union = ...

                def is_first(self) -> bool: ...

                def is_last(self) -> bool: ...

                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            Union_validator: bv.Validator = ...

            class Shape(bb.Union):
                point: Shape = ...

                def is_point(self) -> bool: ...

                def is_circle(self) -> bool: ...

                @classmethod
                def circle(cls, val: float) -> Shape: ...

                def get_circle(self) -> float: ...

                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            Shape_validator: bv.Validator = ...

            """).format(headers=_headers.format(textwrap.dedent("""\
                from typing import (
                    Callable,
                    Text,
                    Type,
                    TypeVar,
                )""")))
        self.assertEqual(result, expected)

    def test__generate_base_namespace_module_with_empty_union(self):
        # type: () -> None
        ns = _make_namespace_with_empty_union()
        result = self._evaluate_namespace(ns)
        expected = textwrap.dedent("""\
            {headers}

            class EmptyUnion(bb.Union):
                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            EmptyUnion_validator: bv.Validator = ...

            """).format(headers=_headers.format(textwrap.dedent("""\
                from typing import (
                    Callable,
                    Text,
                    Type,
                    TypeVar,
                )""")))
        self.assertEqual(result, expected)

    def test__generate_routes(self):
        # type: () -> None
        ns = _make_namespace_with_route()
        result = self._evaluate_namespace(ns)
        expected = textwrap.dedent("""\
            {headers}

            route_one: bb.Route = ...
            route_one_v2: bb.Route = ...

            """).format(headers=_headers.format(textwrap.dedent("""\
                from typing import (
                    TypeVar,
                )""")))
        self.assertEqual(result, expected)

    def test__generate_routes_name_conflict(self):
        # type: () -> None
        ns = _make_namespace_with_route_name_conflict()

        with self.assertRaises(RuntimeError) as cm:
            self._evaluate_namespace(ns)
        self.assertEqual(
            'There is a name conflict between {!r} and {!r}'.format(ns.routes[0], ns.routes[1]),
            str(cm.exception))

    def test__generate_base_namespace_module__with_alias(self):
        # type: () -> None
        ns = _make_namespace_with_alias()
        result = self._evaluate_namespace(ns)
        expected = textwrap.dedent("""\
            {headers}

            class Struct1(bb.Struct):
                def __init__(self,
                             f1: bool = ...) -> None: ...
                f1: bb.Attribute[bool] = ...
                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            Struct1_validator: bv.Validator = ...

            AliasToStruct1_validator: bv.Validator = ...
            AliasToStruct1 = Struct1
            NotUserDefinedAlias_validator: bv.Validator = ...
            """).format(headers=_headers.format(textwrap.dedent("""\
                from typing import (
                    Callable,
                    Text,
                    Type,
                    TypeVar,
                )""")))
        self.assertEqual(result, expected)

    def test__generate_base_namespace_module__with_nullable_and_default_fields(self):
        # type: () -> None
        """
        Make sure that only Nullable fields and fields with default values are Optional in mypy
        """
        ns = _make_namespace_with_nullable_and_dafault_fields()
        result = self._evaluate_namespace(ns)
        expected = textwrap.dedent("""\
            {headers}

            class Struct1(bb.Struct):
                def __init__(self,
                             required_field: int = ...,
                             nullable_field: Optional[int] = ...,
                             default_field: Optional[int] = ...) -> None: ...
                required_field: bb.Attribute[int] = ...
                nullable_field: bb.Attribute[Optional[int]] = ...
                default_field: bb.Attribute[int] = ...
                def _process_custom_annotations(
                    self,
                    annotation_type: Type[T],
                    field_path: Text,
                    processor: Callable[[T, U], U],
                ) -> None: ...

            Struct1_validator: bv.Validator = ...

            """).format(headers=_headers.format(textwrap.dedent("""\
                from typing import (
                    Callable,
                    Optional,
                    Text,
                    Type,
                    TypeVar,
                )""")))
        self.assertEqual(expected, result)
