#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import importlib
import types
from functools import update_wrapper, wraps
from typing import Callable, Type
from unittest import TestCase

import __ae__manifest__


# Taken from:
# https://docs.pylonsproject.org/projects/pyramid/en/latest/_modules/pyramid/decorator.html#reify
# to avoid adding a dependency on libfb.py


class lazy_property:
    """Use as a class method decorator.  It operates almost exactly like the
    Python ``@property`` decorator, but it puts the result of the method it
    decorates into the instance dict after the first call, effectively
    replacing the function it decorates with an instance variable.
    """

    def __init__(self, wrapped):
        self.wrapped = wrapped
        update_wrapper(self, wrapped)

    def __get__(self, inst, objtype=None):
        if inst is None:
            return self
        val = self.wrapped(inst)
        setattr(inst, self.wrapped.__name__, val)
        return val


class ModuleInfo:
    local_path: str
    base_path: str

    def __init__(self, local_path, base_path):
        self.local_path = local_path
        self.base_path = base_path

    @lazy_property
    def path(self) -> str:
        return self.base_path + "/" + self.local_path

    @lazy_property
    def module_name(self) -> str:
        basename, _ = self.local_path.rsplit(".", 1)
        return (self.base_path + "/" + basename).replace("/", ".")

    @lazy_property
    def name(self) -> str:
        basename, _ = self.local_path.rsplit(".", 1)
        return basename.replace("/", ".")

    @lazy_property
    def module(self) -> types.ModuleType:
        return importlib.import_module(self.module_name)

    @lazy_property
    def test_name(self) -> str:
        basename, _ = self.local_path.rsplit(".", 1)
        return f"test_{basename}"

    @lazy_property
    def file(self) -> str:
        return self.module.__file__


_LIST_MODULES = None


def list_modules():
    global _LIST_MODULES
    if _LIST_MODULES is None:
        _LIST_MODULES = [
            ModuleInfo(local_path=f, base_path=__ae__manifest__.BASE_PATH)
            for f in __ae__manifest__.FILES
            if f.endswith(".py")
        ]
    return _LIST_MODULES


T = Type[TestCase]
TestFunc = Callable[[TestCase, ModuleInfo], None]


def _add_test_case(m: ModuleInfo, f: TestFunc, cls: Type[TestCase]) -> None:
    @wraps(f)
    def run_test(test: TestCase):
        # we pass in the TestCase instance instance to `f` to allow it to call methods
        # on it (like `assertEqual`)
        return f(test, m)

    name = m.test_name
    # If you run tsloop with the --focus flag it will only run the tests whose name
    # start with `f`. An easy way to quickly work on a given set of test is to use the
    # focus and run tsloop in the background, that way you can chose which tests are run
    # every time any file in your project is edited.
    # Since our test names are autogenerated we provide the option to rename the module
    # that is being decorated to focus on all the tests in that module.
    if cls.__name__.startswith("f"):
        name = "f" + name
    # Adjust the wrapped function to look it belongs in the class...
    run_test.__module__ = cls.__module__
    run_test.__name__ = name
    run_test.__qualname__ = f"{cls.__qualname__}.{name}"
    setattr(cls, name, run_test)


def populate_test_class(f: TestFunc) -> Callable[[T], T]:
    """
    Autogenerate tests that run on every module of the library.

    Testslide doesn't support the `load_tests protocol`_ so we just use a generic
    function that takes the module_info to run the tests and populate the TestCase with
    one closure pre module.

    Args:
      f: A call back that will be called on every module

    Returns:
      A decorator that will populate a TestCase class with one test per module

    .. _load_tests protocol:
    .. http://docs.python.org/library/unittest.html#load-tests-protocol
    """

    def populate(cls):
        """
        Decorator that adds the tests to a given class.

        Args:
           cls: An TestCase class where we'll inject all of our test functions
        """
        for m in list_modules():
            _add_test_case(m, f, cls)
        return cls

    return populate
