import pytest

from . import mixins


class SlotShadowing(mixins.TargetedNamespaceWalker, mixins.SubclassWalker):
    target_namespace = "snakeoil"
    err_if_slots_is_str = True
    err_if_slots_is_mutable = True

    def recurse_parents(self, kls, seen=None):
        if not seen:
            seen = set()
        for subcls in kls.__bases__:
            if subcls in seen:
                continue
            seen.add(subcls)
            for grand_dad in self.recurse_parents(subcls, seen=seen):
                yield grand_dad
            yield subcls

    @staticmethod
    def mk_name(kls):
        return f"{kls.__module__}.{kls.__name__}"

    def _should_ignore(self, kls):
        return self.mk_name(kls).split(".")[0] != self.target_namespace

    def run_check(self, kls):
        if getattr(kls, "__slotting_intentionally_disabled__", False):
            return

        slotting = {}
        raw_slottings = {}

        for parent in self.recurse_parents(kls):
            slots = getattr(parent, "__slots__", None)

            if slots is None:
                continue

            if isinstance(slots, str):
                slots = (slots,)
            elif isinstance(slots, (dict, list)):
                slots = tuple(slots)

            raw_slottings[slots] = parent
            for slot in slots:
                slotting.setdefault(slot, parent)

        slots = getattr(kls, "__slots__", None)
        if slots is None and not slotting:
            return

        if isinstance(slots, str):
            if self.err_if_slots_is_str:
                pytest.fail(
                    f"cls {kls!r}; slots is {slots!r} (should be a tuple or list)"
                )
            slots = (slots,)

        if slots is None:
            assert not raw_slottings

        if not isinstance(slots, tuple):
            if self.err_if_slots_is_mutable:
                pytest.fail(f"cls {kls!r}; slots is {slots!r}- - should be a tuple")
            slots = tuple(slots)

        if slots is None or (slots and slots in raw_slottings):
            # we do a bool on slots to ignore (); that's completely valid
            # this means that the child either didn't define __slots__, or
            # daftly copied the parents... thus defeating the purpose.
            pytest.fail(
                f"cls {kls!r}; slots is {slots!r}, seemingly inherited from "
                f"{raw_slottings[slots]!r}; the derivative class should be __slots__ = ()"
            )

        for slot in slots:
            if slot in slotting:
                pytest.fail(
                    f"cls {kls!r}; slot {slot!r} was already defined at {slotting[slot]!r}"
                )
