import textwrap

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

import os
import unittest
import subprocess
import sys
import shutil

from stone.ir import (
    ApiNamespace,
    Boolean,
    Struct,
    StructField)
from stone.backends.tsd_types import TSDTypesBackend
from test.backend_test_util import _mock_output
from unittest.mock import Mock


def _make_backend(target_folder_path, template_path, custom_args=None):  # type: ignore
    # type: (typing.Text, typing.Text, typing.List) -> TSDTypesBackend

    args = Mock()
    arg_values = [template_path, "-i=0"]
    if custom_args:
        arg_values = arg_values + custom_args

    args.__iter__ = Mock(return_value=iter(arg_values))

    return TSDTypesBackend(
        target_folder_path=str(target_folder_path),
        args=args
    )


def _make_namespace(ns_name="accounts"):
    # type: (typing.Text) -> ApiNamespace
    ns = ApiNamespace(ns_name)
    struct = _make_struct('User', 'exists', ns)
    ns.add_data_type(struct)
    return ns


def _make_struct(struct_name, struct_field_name, namespace):
    # type: (typing.Text, typing.Text, ApiNamespace) -> Struct
    struct = Struct(name=struct_name, namespace=namespace, ast_node=None)
    struct.set_attributes(None, [StructField(struct_field_name, Boolean(), None, None)])
    return struct


def _evaluate_namespace(backend, namespace_list):
    # type: (TSDTypesBackend, typing.List[ApiNamespace]) -> typing.Text

    get_result = _mock_output(backend)
    filename = "types.d.ts"
    backend.split_by_namespace = False
    backend._generate_base_namespace_module(namespace_list=namespace_list,
                                            filename=filename,
                                            extra_args={},
                                            template="""/*TYPES*/""",
                                            exclude_error_types=True)
    return get_result()


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

    def test__generate_types_single_ns(self):
        # type: () -> None
        backend = _make_backend(target_folder_path="output", template_path="")
        ns = _make_namespace()
        result = _evaluate_namespace(backend, [ns])
        expected = textwrap.dedent("""
        type Timestamp = string;

        namespace accounts {
          export interface User {
            exists: boolean;
          }

        }


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

    def test__generate_types_empty_ns(self):
        # type: () -> None
        backend = _make_backend(target_folder_path="output", template_path="")
        empty_ns = ApiNamespace("empty_namespace")
        result = _evaluate_namespace(backend, [empty_ns])
        expected = textwrap.dedent("")
        self.assertEqual(result, expected)

    def test__generate_types_with_empty_ns(self):
        # type: () -> None
        backend = _make_backend(target_folder_path="output", template_path="")
        ns = _make_namespace()
        empty_ns = ApiNamespace("empty_namespace")
        result = _evaluate_namespace(backend, [ns, empty_ns])
        expected = textwrap.dedent("""
        type Timestamp = string;

        namespace accounts {
          export interface User {
            exists: boolean;
          }

        }


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

    def test__generate_types_multiple_ns(self):
        # type: () -> None
        backend = _make_backend(target_folder_path="output", template_path="")
        ns1 = _make_namespace("accounts")
        ns2 = _make_namespace("files")
        result = _evaluate_namespace(backend, [ns1, ns2])
        expected = textwrap.dedent("""
        type Timestamp = string;

        namespace accounts {
          export interface User {
            exists: boolean;
          }

        }

        namespace files {
          export interface User {
            exists: boolean;
          }

        }


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

    def test__generate_types_multiple_ns_with_export(self):
        # type: () -> None
        backend = _make_backend(target_folder_path="output", template_path="",
                                custom_args=["--export-namespaces"])
        ns1 = _make_namespace("accounts")
        ns2 = _make_namespace("files")
        result = _evaluate_namespace(backend, [ns1, ns2])
        expected = textwrap.dedent("""
        type Timestamp = string;

        export namespace accounts {
          export interface User {
            exists: boolean;
          }

        }

        export namespace files {
          export interface User {
            exists: boolean;
          }

        }


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


class SpecHelper:
    """
    A helper class which exposes two namespace definitions
    and its corresponding type definitions for testing. The
    types are available as either a declaration or a namespace.
    """

    def __init__(self):
        pass

    _error_types = """
/**
 * An Error object returned from a route.
 */
interface Error<T> {
  // Text summary of the error.
  error_summary: string;
  // The error object.
  error: T;
  // User-friendly error message.
  user_message: UserMessage;
}

/**
 * User-friendly error message.
 */
interface UserMessage {
  // The message.
  text: string;
  // The locale of the message.
  locale: string;
}

"""

    _ns_spec = """\
namespace ns
import ns2
struct A
    "Sample struct doc."
    a String
        "Sample field doc."
    b Int64
struct B extends ns2.BaseS
    c Bytes
"""

    _ns_spec_types = """{
  /**
   * Sample struct doc.
   */
  export interface A {
    /**
     * Sample field doc.
     */
    a: string;
    b: number;
  }

  export interface B extends ns2.BaseS {
    c: string;
  }
%s
}
"""

    _ns2_spec = """\
namespace ns2
struct BaseS
    "This is a test."
    z Int64
    maptype Map(String, Int64)
union_closed BaseU
    z
    x String
alias AliasedBaseU = BaseU
    """

    _ns2_spec_types = """{
  /**
   * This is a test.
   */
  export interface BaseS {
    z: number;
    maptype: {[key: string]: number};
  }

  export interface BaseUZ {
    '.tag': 'z';
  }

  export interface BaseUX {
    '.tag': 'x';
    x: string;
  }

  export type BaseU = BaseUZ | BaseUX;

  export type AliasedBaseU = BaseU;
%s
}
"""

    _ns3_union_spec = """\
namespace ns3
struct A
    union
        a1 A1
        a2 A2
    a String
struct A1 extends A
    b Boolean
struct A2 extends A
    c Boolean
    mapfield Map(String, A)
union M
    e Boolean
    f String
union B
    w Boolean
    x A
    y M
    z A2
"""

    _ns3_union_spec_types = """{
  export interface A {
    a: string;
  }

  /**
   * Reference to the A polymorphic type. Contains a .tag property to let you
   * discriminate between possible subtypes.
   */
  export interface AReference extends A {
    /**
     * Tag identifying the subtype variant.
     */
    '.tag': "a1"|"a2";
  }

  export interface A1 extends A {
    b: boolean;
  }

  /**
   * Reference to the A1 type, identified by the value of the .tag property.
   */
  export interface A1Reference extends A1 {
    /**
     * Tag identifying this subtype variant. This field is only present when
     * needed to discriminate between multiple possible subtypes.
     */
    '.tag': 'a1';
  }

  export interface A2 extends A {
    c: boolean;
    mapfield: {[key: string]: A};
  }

  /**
   * Reference to the A2 type, identified by the value of the .tag property.
   */
  export interface A2Reference extends A2 {
    /**
     * Tag identifying this subtype variant. This field is only present when
     * needed to discriminate between multiple possible subtypes.
     */
    '.tag': 'a2';
  }

  export interface BW {
    '.tag': 'w';
    w: boolean;
  }

  export interface BX {
    '.tag': 'x';
    x: A1Reference|A2Reference|AReference;
  }

  export interface BY {
    '.tag': 'y';
    y: M;
  }

  export interface BZ extends A2 {
    '.tag': 'z';
  }

  export interface BOther {
    '.tag': 'other';
  }

  export type B = BW | BX | BY | BZ | BOther;

  export interface ME {
    '.tag': 'e';
    e: boolean;
  }

  export interface MF {
    '.tag': 'f';
    f: string;
  }

  export interface MOther {
    '.tag': 'other';
  }

  export type M = ME | MF | MOther;
%s
}
"""

    _timestamp_mapping = 'type Timestamp = string'

    _timestamp_def_formatted = "\n" + "  " + _timestamp_mapping + ";"

    @classmethod
    def get_ns_spec(cls):
        """Returns a test namespace which imports another namespace (`ns2`)."""
        return cls._ns_spec

    @classmethod
    def get_ns_types_as_declaration(cls):
        types = """\nimport * as ns2 from 'ns2';\n""" + (
            ("\ndeclare module 'ns' " + cls._ns_spec_types) % cls._timestamp_def_formatted) + "\n\n"
        return types.replace('namespace', 'declare module')

    @classmethod
    def get_ns2_spec(cls):
        """Returns a simple namespace."""
        return cls._ns2_spec

    @classmethod
    def get_ns2_types_as_declaration(cls):
        return (("\ndeclare module 'ns2' " + cls._ns2_spec_types
                 ) % cls._timestamp_def_formatted) + "\n\n"

    @classmethod
    def get_all_types_as_namespace(cls):
        types = cls._error_types + "\n" + cls._timestamp_mapping + ";\n" + (
            ("\nnamespace ns " + cls._ns_spec_types) % "") + (
            ("\nnamespace ns2 " + cls._ns2_spec_types) % "") + "\n\n"
        return types

    @classmethod
    def get_ns3_spec_for_union(cls):
        """
        Returns a test namespace which has a union field with all possible types of
        members a union can have. It includes (1) primitive, (2) struct, (3) enumerated
        subtypes, and (4) a union. This spec is useful in validating the auto-generated
        code for a union type defined in a namespace.
        """
        return cls._ns3_union_spec

    @classmethod
    def get_ns3_types_as_declaration(cls):
        return (("\ndeclare module 'ns3' " + cls._ns3_union_spec_types
                 ) % cls._timestamp_def_formatted) + "\n\n"


class TestTSDTypesE2E(unittest.TestCase):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.maxDiff = None  # Increase text diff size

    def setUp(self):
        self.stone_output_directory = "output"
        if not os.path.exists(self.stone_output_directory):
            os.makedirs(self.stone_output_directory)
        self.template_file_name = "typescript.template"
        template_file_path = "{}/{}".format(self.stone_output_directory, self.template_file_name)
        with open(template_file_path, "w", encoding='utf-8') as template_file:
            template_file.write("/*TYPES*/")

    def tearDown(self):
        # Clear output of stone tool after all tests.
        shutil.rmtree('output')

    def _verify_generated_output(self, filename, expected_namespace_types):
        with open(filename, encoding='utf-8') as f:
            generated_types = f.read()
            self.assertEqual(generated_types, expected_namespace_types)

    def test_tsd_types_declarations_output(self):
        # Sanity check: stone must be importable for the compiler to work
        __import__('stone')

        # Compile spec by calling out to stone
        p = subprocess.Popen(
            [sys.executable,
             '-m',
             'stone.cli',
             'tsd_types',
             self.stone_output_directory,
             '--',
             self.template_file_name,
             '--exclude_error_types',
             '-i=0'],
            stdin=subprocess.PIPE,
            stderr=subprocess.PIPE)
        _, stderr = p.communicate(
            input=(SpecHelper.get_ns_spec() + SpecHelper.get_ns2_spec()).encode('utf-8'))
        if p.wait() != 0:
            raise AssertionError('Could not execute stone tool: %s' %
                                 stderr.decode('utf-8'))

        # one file must be generated per namespace
        expected_ns_output = SpecHelper.get_ns_types_as_declaration()
        self._verify_generated_output('output/ns.d.ts', expected_ns_output)

        expected_ns2_output = SpecHelper.get_ns2_types_as_declaration()
        self._verify_generated_output('output/ns2.d.ts', expected_ns2_output)

    def test_tsd_types_namespace_output(self):
        # Sanity check: stone must be importable for the compiler to work
        __import__('stone')

        output_file_name = "all_types.ts"

        # Compile spec by calling out to stone
        p = subprocess.Popen(
            [sys.executable,
             '-m',
             'stone.cli',
             'tsd_types',
             self.stone_output_directory,
             '--',
             self.template_file_name,
             output_file_name,
             '-i=0'],
            stdin=subprocess.PIPE,
            stderr=subprocess.PIPE)
        _, stderr = p.communicate(
            input=(SpecHelper.get_ns_spec() + SpecHelper.get_ns2_spec()).encode('utf-8'))
        if p.wait() != 0:
            raise AssertionError('Could not execute stone tool: %s' %
                                 stderr.decode('utf-8'))

        expected_output = SpecHelper.get_all_types_as_namespace()
        self._verify_generated_output('output/{}'.format(output_file_name), expected_output)

    def test_tsd_types_for_union(self):
        """
        Test tsd types generated for a union which has all possible data types as
        members including primitive, struct, enumerated sub types and unions.
        """
        # Sanity check: stone must be importable for the compiler to work
        __import__('stone')

        # Compile spec by calling out to stone
        p = subprocess.Popen(
            [sys.executable,
             '-m',
             'stone.cli',
             'tsd_types',
             self.stone_output_directory,
             '--',
             self.template_file_name,
             '--exclude_error_types',
             '-i=0'],
            stdin=subprocess.PIPE,
            stderr=subprocess.PIPE)
        _, stderr = p.communicate(
            input=(SpecHelper.get_ns3_spec_for_union()).encode('utf-8'))
        if p.wait() != 0:
            raise AssertionError('Could not execute stone tool: %s' %
                                 stderr.decode('utf-8'))

        # one file must be generated per namespace
        expected_ns_output = SpecHelper.get_ns3_types_as_declaration()
        self._verify_generated_output('output/ns3.d.ts', expected_ns_output)


if __name__ == '__main__':
    unittest.main()
