"""Test the memory_debug utility.

Running it this way reveals the stdout messages about MemoryDebugNode IDs:
        uvenv python -m wholecell.tests.utils.test_memory_debug

In any case, you should see GC messages like:
        gc: uncollectable <MemoryDebugNode 0x110118050>
        gc: uncollectable <MemoryDebugNode 0x110118090>
        gc: uncollectable <MemoryDebugNode 0x110118110>
        gc: uncollectable <dict 0x1101124b0>
        gc: uncollectable <dict 0x110112a28>

# The test case is adapted from https://pymotw.com/2/gc/

Per https://docs.python.org/2/library/gc.html, "Objects that have __del__()
methods and are part of a reference cycle cause the entire reference cycle
to be uncollectable, including objects not necessarily in the cycle but
reachable only from it. Python doesn't collect such cycles automatically
because, in general, it isn't possible for Python to guess a safe order in
which to run the __del__() methods."

Per https://docs.python.org/3/library/gc.html, "Changed in version 3.4:
Following PEP 442, objects with a __del__() method don't end up in gc.garbage
anymore."
"""

import gc
import sys
import unittest

from wholecell.utils import memory_debug

PEP442 = sys.version_info[:2] >= (3, 4)

# Silence Sphinx autodoc warning
unittest.TestCase.__module__ = "unittest"


class MemoryDebugNode:
    def __init__(self, name):
        self.name = str(name)
        self.link = None
        self.link2 = None

    def __repr__(self):
        """The repr string includes the object address and its __dict__ address
        to match up to "gc: uncollectable" printouts from the GC.
        """
        return f"{type(self).__name__}{self.name} 0x{id(self):x} dict 0x{id(self.__dict__):x}"

    def __del__(self):
        """Log a message to show that this __del__() method got called."""
        print(f"    {self} __del__()")


class Test_memory_debug(unittest.TestCase):
    def test_memory_debug(self):
        precount = len(gc.garbage)

        with memory_debug.detect_leaks(enabled=True):
            nodes = [MemoryDebugNode(i) for i in range(6)]

            # N0 -> N1 -> N2 are not in a cycle and should be collectable.
            nodes[0].link = nodes[1]
            nodes[1].link = nodes[2]

            # Make a cycle + spur N3 <-> N4 -> N5 of uncollectable Nodes.
            # N3's and N4's dicts will be in the cycle.
            nodes[3].link = nodes[4]
            nodes[4].link = nodes[3]
            nodes[4].link2 = nodes[5]

            print(f"Test_memory_debug dropping refs. This should log __del__() on {nodes[:3]}.")
            uncollectable = str(nodes[3:])  # don't retain the Nodes
            nodes[:] = []

            if PEP442:
                print("Test_memory_debug GC'ing.")
            else:
                print(f"Test_memory_debug GC'ing. {uncollectable} and some of their dicts should log as uncollectable.")
                # Why is Node5's dict collectable?

        # gc.garbage holds Node3 .. Node5.
        added_count = 0 if PEP442 else 3
        assert len(gc.garbage) == precount + added_count, f"Uncollectable: {gc.garbage}"


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