import collections
import inspect
import itertools

import odoo
from odoo.tests.common import get_db_name, tagged
from .lint_case import LintCase


POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY
POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD
VAR_POSITIONAL = inspect.Parameter.VAR_POSITIONAL
KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY
VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD


failure_message = """\
Invalid override of {model}.{method} in {child_module}, {message}.

Original definition in {parent_module}:
    def {method}{original_signature}

Incompatible definition in {child_module}:
    def {method}{override_signature}"""


methods_to_sanitize = {
    method_name
    for method_name in dir(odoo.models.BaseModel)
    if not method_name.startswith('_')
} - {
    # Not yet sanitized...
    'write', 'create', 'default_get', 'init'
}


class HitMiss:
    def __init__(self):
        self.hit = 0
        self.miss = 0
    @property
    def ratio(self):
        return self.hit / (self.hit + self.miss)
counter = collections.defaultdict(HitMiss)


def get_odoo_module_name(python_module_name):
    if python_module_name.startswith('odoo.addons.'):
        return python_module_name.split('.')[2]
    if python_module_name == 'odoo.models':
        return 'odoo'

    return python_module_name


def assert_valid_override(parent_signature, child_signature):
    pparams = parent_signature.parameters
    cparams = child_signature.parameters

    # parent and child have exact same signature
    if pparams == cparams:
        return

    # child overrides is exactly (self, *args, **kwargs)
    if [cp.kind for cp in cparams.values()] == [VAR_POSITIONAL, VAR_KEYWORD]:
        return

    # parent has *args/**kwargs: child can define new custom args/kwargs
    parent_has_varargs = any(pp.kind == VAR_POSITIONAL for pp in pparams.values())
    parent_has_varkwargs = any(pp.kind == VAR_KEYWORD for pp in pparams.values())

    # child has *args/**kwargs: all unknown args/kargs are delegated
    child_has_varargs = any(cp.kind == VAR_POSITIONAL for cp in cparams.values())
    child_has_varkwargs = any(cp.kind == VAR_KEYWORD for cp in cparams.values())

    # check positionals
    pposparams = [(pp_name, pp) for pp_name, pp in pparams.items()
                  if pp.kind in (POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD)]
    cposparams = [(cp_name, cp) for cp_name, cp in cparams.items()
                  if cp.kind in (POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD)]
    if len(cposparams) < len(pposparams):
        assert child_has_varargs, "missing positional parameters"
        assert cposparams == pposparams[:len(cposparams)], "wrong positional parameters"
    elif len(cposparams) > len(pposparams):
        assert parent_has_varargs, "too many positional parameters"
        assert cposparams[:len(pposparams)] == pposparams, "wrong positional parameters"
    else:
        assert cposparams == pposparams, "wrong positional parameters"

    # check keywords
    pkwparams = {(pp_name, pp) for pp_name, pp in pparams.items()
                 if pp.kind in (POSITIONAL_OR_KEYWORD, KEYWORD_ONLY)}
    ckwparams = {(cp_name, cp) for cp_name, cp in cparams.items()
                 if cp.kind in (POSITIONAL_OR_KEYWORD, KEYWORD_ONLY)}
    if ckwparams < pkwparams:
        assert child_has_varkwargs, "missing keyword parameters"
    elif ckwparams > pkwparams:
        assert parent_has_varkwargs, "too many keyword parameters"
    elif ckwparams != pkwparams:
        assert child_has_varkwargs and parent_has_varkwargs, "wrong keyword parameters"


@tagged('-at_install', 'post_install')
class TestLintOverrideSignatures(LintCase):
    def test_lint_override_signature(self):
        self.failureException = TypeError
        registry = odoo.registry(get_db_name())
        for model_name, model_cls in registry.items():
            for method_name, _ in inspect.getmembers(model_cls, inspect.isroutine):
                if method_name not in methods_to_sanitize:
                    continue

                # Find the original function definition
                reverse_mro = reversed(model_cls.mro()[1:-1])
                for parent_class in reverse_mro:
                    method = getattr(parent_class, method_name, None)
                    if callable(method):
                        break

                parent_module = get_odoo_module_name(parent_class.__module__)
                original_signature = inspect.signature(method)

                iter_parent, iter_children = itertools.tee(reverse_mro)
                iter_parent = itertools.chain([parent_class], iter_parent)

                # Assert that all child classes correctly override the method
                for parent_class, child_class in zip(iter_parent, iter_children):
                    if method_name not in child_class.__dict__:
                        continue
                    override = getattr(child_class, method_name)

                    child_module = get_odoo_module_name(child_class.__module__)
                    override_signature = inspect.signature(override)

                    with self.subTest(module=child_module, model=model_name, method=method_name):
                        try:
                            assert_valid_override(original_signature, override_signature)
                            counter[method_name].hit += 1
                        except AssertionError as exc:
                            counter[method_name].miss += 1
                            raise TypeError(failure_message.format(
                                message=exc.args[0],
                                model=model_name,
                                method=method_name,
                                child_module=child_module,
                                parent_module=parent_module,
                                original_signature=original_signature,
                                override_signature=override_signature,
                            )) from None

        #with open('/tmp/odoo-override-signatures.md', 'w') as f:
        #    f.write('method|hit|miss|ratio\n')
        #    f.write('------|---|----|-----\n')
        #    for method_name, hm in sorted(counter.items(), key=lambda x: x[1].miss):
        #        f.write(f'{method_name}|{hm.hit}|{hm.miss}|{hm.ratio:.3f}\n')
