from functools import partial

import pytest

from fastNLP.core.utils.utils import auto_param_call, _check_valid_parameters_number, _get_fun_msg
from fastNLP.core.metrics import Metric



class TestAutoParamCall:
    def test_basic(self):
        def fn(x):
            return x
        x = {'x': 3, 'y': 4}
        r = auto_param_call(fn, x)
        assert r==3

        xs = []
        for i in range(10):
            xs.append({f'x{i}': i})
        def fn(x0, x1, x2, x3):
            return x0 + x1 + x2 + x3
        r = auto_param_call(fn, *xs)
        assert r == 0 + 1+ 2+ 3

        def fn(chongfu1, chongfu2, buChongFu):
            pass
        with pytest.raises(BaseException) as exc_info:
            auto_param_call(fn, {'chongfu1': 3, "chongfu2":4, 'buChongFu':2},
                            {'chongfu1': 1, 'chongfu2':2, 'buChongFu':2})
        assert 'The following key present in several inputs' in exc_info.value.args[0]
        assert 'chongfu1' in exc_info.value.args[0] and  'chongfu2' in exc_info.value.args[0]

        # 没用到不报错
        def fn(chongfu1, buChongFu):
            pass
        auto_param_call(fn, {'chongfu1': 1, "chongfu2":4, 'buChongFu':2},
                        {'chongfu1': 1, 'chongfu2':2, 'buChongFu':2})

        # 可以定制signature_fn
        def fn1(**kwargs):
            kwargs.pop('x')
            kwargs.pop('y')
            assert len(kwargs)==0
        def fn(x, y):
            pass
        x = {'x': 3, 'y': 4}
        r = auto_param_call(fn1, x, signature_fn=fn)

        # 没提供的时候报错
        def fn(meiti1, meiti2, tigong):
            pass
        with pytest.raises(BaseException) as exc_info:
            auto_param_call(fn, {'tigong':1})
        assert 'meiti1' in exc_info.value.args[0] and 'meiti2' in exc_info.value.args[0]

        # 默认值替换
        def fn(x, y=100):
            return x + y
        r = auto_param_call(fn, {'x': 10, 'y': 20})
        assert r==30
        assert auto_param_call(fn, {'x': 10, 'z': 20})==110

        # 测试mapping的使用
        def fn(x, y=100):
            return x + y
        r = auto_param_call(fn, {'x1': 10, 'y1': 20}, mapping={'x1': 'x', 'y1': 'y', 'meiyong': 'meiyong'})
        assert r==30

        # 测试不需要任何参数
        def fn():
            return 1
        assert 1 == auto_param_call(fn, {'x':1})

        # 测试调用类的方法没问题
        assert 2==auto_param_call(self.call_this, {'x':1 ,'y':1})
        assert 2==auto_param_call(self.call_this, {'x':1,'y':1, 'z':1},mapping={'z': 'self'})

    def test_msg(self):
        with pytest.raises(BaseException) as exc_info:
            auto_param_call(self.call_this, {'x':1})
        assert 'TestAutoParamCall.call_this' in exc_info.value.args[0]

        with pytest.raises(BaseException) as exc_info:
            auto_param_call(call_this_for_auto_param_call, {'x':1})
        assert __file__ in exc_info.value.args[0]
        assert 'call_this_for_auto_param_call' in exc_info.value.args[0]

        with pytest.raises(BaseException) as exc_info:
            auto_param_call(self.call_this_two, {'x':1})
        assert __file__ in exc_info.value.args[0]

        with pytest.raises(BaseException) as exc_info:
            auto_param_call(call_this_for_auto_param_call, {'x':1}, signature_fn=self.call_this)
        assert 'TestAutoParamCall.call_this' in exc_info.value.args[0]  # 应该是signature的信息

    def call_this(self, x, y):
        return x + y

    def call_this_two(self, x, y, z=pytest, **kwargs):
        return x + y

    def test_metric_auto_param_call(self):
        metric = AutoParamCallMetric()
        with pytest.raises(BaseException):
            auto_param_call(metric.update, {'y':1}, signature_fn=metric.update.__wrapped__)


class AutoParamCallMetric(Metric):
    def update(self, x):
        pass


def call_this_for_auto_param_call(x, y):
    return x + y


class TestCheckNumberOfParameters:
    def test_validate_every(self):
        def validate_every(trainer):
            pass
        _check_valid_parameters_number(validate_every, expected_params=['trainer'])

        # 无默认值，多了报错
        def validate_every(trainer, other):
            pass
        with pytest.raises(TypeError) as exc_info:
            _check_valid_parameters_number(validate_every, expected_params=['trainer'])
        print(exc_info.value.args[0])

        # 有默认值ok
        def validate_every(trainer, other=1):
            pass
        _check_valid_parameters_number(validate_every, expected_params=['trainer'])

        # 参数多了
        def validate_every(trainer):
            pass
        with pytest.raises(TypeError) as exc_info:
            _check_valid_parameters_number(validate_every, expected_params=['trainer', 'other'])
        print(exc_info.value.args[0])

        # 使用partial
        def validate_every(trainer, other):
            pass
        _check_valid_parameters_number(partial(validate_every, other=1), expected_params=['trainer'])
        with pytest.raises(TypeError):
            _check_valid_parameters_number(partial(validate_every, other=1), expected_params=['trainer', 'other'])
        with pytest.raises(TypeError) as exc_info:
            _check_valid_parameters_number(partial(validate_every, other=1), expected_params=['trainer', 'other', 'more'])
        print(exc_info.value.args[0])

        # 如果存在 *args 或 *kwargs 不报错多的
        def validate_every(trainer, *args):
            pass
        _check_valid_parameters_number(validate_every, expected_params=['trainer', 'other', 'more'])

        def validate_every(trainer, **kwargs):
            pass
        with pytest.raises(TypeError):
            _check_valid_parameters_number(partial(validate_every, trainer=1), expected_params=['trainer', 'other', 'more'])

        # class 的方法删掉self
        class InnerClass:
            def demo(self, x):
                pass

            def no_param(self):
                pass

            def param_kwargs(self, **kwargs):
                pass

        inner = InnerClass()
        with pytest.raises(TypeError) as exc_info:
            _check_valid_parameters_number(inner.demo, expected_params=['trainer', 'other', 'more'])
        _check_valid_parameters_number(inner.demo, expected_params=['trainer'])


def test_get_fun_msg():
    # 测试运行
    def demo(x):
        pass

    print(_get_fun_msg(_get_fun_msg))