#!/usr/bin/env python3

# Unit tests for decopy
# Copyright: 2016, Margarita Manterola <marga@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.

import unittest

import debian.copyright as dc

from decopy import cmdoptions, core_types, dep5, holders, nodes, tree
from decopy.group import TallyTracker
from decopy.group_contracts import GroupProtocol
from decopy.renderer import Dep5Renderer


class TestCopyright(unittest.TestCase):
    def _process_tree(self, path):
        """Process the received tree and parse the copyright file."""
        options = cmdoptions.process_options(["--no-progress", "--root", path])
        filetree = tree.RootInfo.build(options)
        copyright_ = dep5.Copyright.build(filetree, options)
        copyright_.process(filetree)
        return copyright_, options

    def test_process_empty(self):
        """Parses a tree with no debian/copyright file."""

        copyright_, _ = self._process_tree("tests/testdata/two")

        # An empty copyright file should yield no groups after processing
        self.assertEqual(copyright_.groups, [])

    def test_process_malformed(self):
        """Parses a tree with a malformed debian/copyright file."""

        copyright_, _ = self._process_tree("tests/testdata/malformed")

        # An empty copyright file should yield no groups after processing
        self.assertEqual(copyright_.groups, [])

    def test_groups_are_group_protocol_instances(self):
        """After processing, every element of groups implements GroupProtocol."""
        copyright_, _ = self._process_tree("tests/testdata/incomplete")
        assert copyright_.groups is not None
        for group in copyright_.groups:
            self.assertIsInstance(group, GroupProtocol)

    def test_process_incomplete(self):
        """Parses a tree with an incomplete debian/copyright file."""

        copyright_, options = self._process_tree("tests/testdata/incomplete")

        # An incomplete copyright file should yield the incomplete groups after
        # processing
        expected_groups = [
            {
                "license": "Unknown",
                "holders": [],
                "files": ["debian/copyright"],
                "comment": "",
            },
            {
                "license": "Unknown",
                "holders": [],
                "files": ["test"],
                "comment": "",
            },
        ]
        self._check_sorted_groups(expected_groups, copyright_.groups, options)

    def test_process_simple(self):
        """Parses a debian/copyright with two entries (testdata/four)."""

        copyright_, options = self._process_tree("tests/testdata/four")

        expected_groups = [
            {
                "license": "LGPL-2+",
                "holders": ["2014-2016, Maximiliano Curia <maxy@debian.org>"],
                "files": ["debian/compat", "debian/copyright"],
                "comment": "",
            },
            {
                "license": "GPL-2+",
                "holders": ["2016, Jane Doe <janedoe@example.com>"],
                "files": ["test"],
                "comment": "",
            },
        ]

        self._check_sorted_groups(expected_groups, copyright_.groups, options)

    def test_process_complex(self):
        """Parses a debian/copyright with many entries (testdata/three)."""

        copyright_, options = self._process_tree("tests/testdata/three")

        expected_groups = [
            {
                "license": "LGPL-2+",
                "holders": ["2014-2016, Maximiliano Curia <maxy@debian.org>"],
                "files": ["debian/compat", "debian/copyright", "debian/rules"],
                "comment": "No real packaging, just empty files.",
            },
            {
                "license": "GPL-2+",
                "holders": ["1989-2003, Free Software Foundation, Inc"],
                "files": ["acinclude.m4"],
                "comment": "",
            },
            {
                "license": "MIT/X11",
                "holders": ["1993-2004, Clara Oswald <clara@example.com>"],
                "files": ["src/do-something.m", "src/do-something.py"],
                "comment": "",
            },
            {
                "license": "GPL-2+",
                "holders": ["2016, Jane Doe <janedoe@example.com>"],
                "files": ["COPYING", "data/file.txt", "src/main.py", "src/module.py"],
                "comment": "",
            },
        ]

        self._check_sorted_groups(expected_groups, copyright_.groups, options)

    def _check_sorted_groups(self, expected_groups, groups, options):
        for i, group in enumerate(groups):
            key = group.get_key(options)
            expected = expected_groups[i]
            with self.subTest(msg=f'Checking "{key}"'):
                self.assertEqual(
                    list(group.copyrights.sorted_members()),
                    expected["holders"],
                )
                self.assertEqual(group.license, expected["license"])
                self.assertEqual(group.files.sorted_members(), expected["files"])
                self.assertEqual(group.get_comments(), expected["comment"])

    def _process_and_remove(self, path, opts):
        """Lists the tree, parses the copyright, reads all files, removes
        misplaced."""
        options = cmdoptions.process_options(["--no-progress", "--root", path, *opts])
        filetree = tree.RootInfo.build(options)
        copyright_ = dep5.Copyright.build(filetree, options)
        copyright_.process(filetree)
        filetree.process(options)
        # This function removes files from the groups when they should no longer
        # be there.  It does not add them to new or existing groups.  So the
        # effect that we will see is that some files or groups go "missing".
        copyright_.remove_misplaced_files(options)
        return copyright_, options

    def _check_expected_groups(self, expected_groups, groups):
        """Checks that the received and expected groups match."""
        # Check that we got all expected groups
        self.longMessage = False
        for key in expected_groups:
            with self.subTest(f'Checking "{key}"'):
                self.assertIn(key, groups, msg=f"missing expected group: {key}")
        for key in groups:
            with self.subTest(f'Checking "{key}"'):
                self.assertIn(
                    key,
                    expected_groups,
                    msg=f"unexpected group: {key}",
                )
        self.longMessage = True

        # Check that the groups are what we expected
        for key, group in groups.items():
            with self.subTest(msg=f'Checking "{key}"'):
                expected = expected_groups[key]
                self.assertEqual(
                    list(group.copyrights.sorted_members()),
                    expected["holders"],
                )
                self.assertEqual(group.license, expected["license"])
                self.assertEqual(group.files.sorted_members(), expected["files"])
                self.assertEqual(group.get_comments(), expected["comment"])

    def test_remove_misplaced(self):
        """Removes files from copyright if they were wrong (testdata/three)."""

        copyright_, options = self._process_and_remove("tests/testdata/three", [])

        expected_groups = {
            ("LGPL-2+", None, "debian"): {
                "license": "LGPL-2+",
                "holders": ["2014-2016, Maximiliano Curia <maxy@debian.org>"],
                "files": ["debian/compat", "debian/copyright", "debian/rules"],
                "comment": "No real packaging, just empty files.",
            },
            ("GPL-2+", None, ""): {
                "license": "GPL-2+",
                "holders": [
                    "1989-2003, Free Software Foundation, Inc",
                    "2016, Jane Doe <janedoe@example.com>",
                ],
                "files": ["COPYING", "acinclude.m4", "src/main.py", "src/module.py"],
                "comment": "",
            },
            ("MIT/X11", None, ""): {
                "license": "MIT/X11",
                "holders": ["1993-2004, Clara Oswald <clara@example.com>"],
                "files": ["src/do-something.m", "src/do-something.py"],
                "comment": "",
            },
        }
        groups = copyright_.get_group_dict(options)
        self._check_expected_groups(expected_groups, groups)

    def test_remove_misplaced_copyright(self):
        """Removes files if they were wrong, grouping by copyright."""

        copyright_, options = self._process_and_remove(
            "tests/testdata/three",
            ["--group-by", "copyright"],
        )

        expected_groups = {
            ("LGPL-2+", ("maxy@debian.org",), "debian"): {
                "license": "LGPL-2+",
                "holders": ["2014-2016, Maximiliano Curia <maxy@debian.org>"],
                "files": ["debian/compat", "debian/copyright", "debian/rules"],
                "comment": "No real packaging, just empty files.",
            },
            ("GPL-2+", ("janedoe@example.com",), ""): {
                "license": "GPL-2+",
                "holders": ["2016, Jane Doe <janedoe@example.com>"],
                "files": ["COPYING", "src/main.py", "src/module.py"],
                "comment": "",
            },
            ("MIT/X11", ("clara@example.com",), ""): {
                "license": "MIT/X11",
                "holders": ["1993-2004, Clara Oswald <clara@example.com>"],
                "files": ["src/do-something.m", "src/do-something.py"],
                "comment": "",
            },
            ("GPL-2+", ("Free Software Foundation, Inc",), ""): {
                "license": "GPL-2+",
                "holders": ["1989-2003, Free Software Foundation, Inc"],
                "files": ["acinclude.m4"],
                "comment": "",
            },
        }
        groups = copyright_.get_group_dict(options)
        self._check_expected_groups(expected_groups, groups)

    def test_remove_misplaced_partial(self):
        """Removes files if they were wrong, grouping by copyright."""

        copyright_, options = self._process_and_remove(
            "tests/testdata/three",
            ["--mode", "partial", "tests/testdata/three/debian"],
        )

        expected_groups = {
            ("LGPL-2+", None, "debian"): {
                "license": "LGPL-2+",
                "holders": ["2014-2016, Maximiliano Curia <maxy@debian.org>"],
                "files": ["debian/compat", "debian/copyright", "debian/rules"],
                "comment": "No real packaging, just empty files.",
            },
            ("GPL-2+", None, ""): {
                "license": "GPL-2+",
                "holders": ["1989-2003, Free Software Foundation, Inc"],
                "files": [],
                "comment": "",
            },
            ("MIT/X11", None, ""): {
                "license": "MIT/X11",
                "holders": ["1993-2004, Clara Oswald <clara@example.com>"],
                "files": [],
                "comment": "",
            },
        }

        groups = copyright_.get_group_dict(options)
        self._check_expected_groups(expected_groups, groups)

    def test_get_group_dict_returns_dict_of_groups(self):
        """get_group_dict returns dict with Group values and tuple keys."""
        copyright_, options = self._process_tree("tests/testdata/four")
        groups = copyright_.get_group_dict(options)
        self.assertIsInstance(groups, dict)
        for key, value in groups.items():
            self.assertIsInstance(key, tuple)
            self.assertIsInstance(value, dep5.Group)


class TestGroup(unittest.TestCase):
    def test_unknown_license(self):
        g = dep5.Group()
        self.assertEqual(g.license, core_types.UNKNOWN)
        cg = holders.CopyrightHolders()
        cg.add(core_types.CopyrightHolder("", "foo@example.com"))
        g.add_copyrights(cg)
        self.assertEqual(g.license, core_types.UNKNOWN_COPYRIGHTED)

    def test_add_copyrights_accepts_copyright_holder_list(self):
        g = dep5.Group()
        holders = [
            core_types.CopyrightHolder("Alice", "alice@example.com"),
            core_types.CopyrightHolder("Bob", "bob@example.com"),
        ]
        g.add_copyrights(holders)
        self.assertEqual(len(g.copyrights), 2)

    def test_add_copyrights_accepts_copyright_group(self):
        g = dep5.Group()
        cg = holders.CopyrightHolders()
        cg.add(core_types.CopyrightHolder("Carol", "carol@example.com"))
        g.add_copyrights(cg)
        self.assertEqual(len(g.copyrights), 1)

    def test_sort_key_returns_five_tuple(self):
        g = dep5.Group(position=1)
        g.licenses["MIT"] = core_types.License.get("MIT")
        root = tree.RootInfo(root=".")
        fi = nodes.FileInfo(root, "foo.c")
        g.files.add_file(fi)
        options = cmdoptions.process_options(["--no-progress", "--root", "."])
        key = g.sort_key(options)
        self.assertIsInstance(key, tuple)
        self.assertEqual(len(key), 5)
        self.assertIsInstance(key[0], int)
        self.assertIsInstance(key[1], int)


class TestGroupCopyrightBlock(unittest.TestCase):
    """Unit tests for the DEP-5 Files paragraph (Dep5Renderer.file_block).

    These tests construct Group objects directly (no file I/O) so that each
    test exercises exactly one aspect of the generated DEP-5 block string.
    group.files.add_file() is used instead of group.add_file() intentionally:
    the latter merges licenses and copyrights from the FileInfo, which would
    interfere with the explicit values set by the tests.
    """

    def setUp(self):
        self._saved_licenses: dict[str, core_types.License] = (
            core_types.License.licenses
        )
        core_types.License.licenses = {}

    def tearDown(self):
        core_types.License.licenses = self._saved_licenses

    def _block(self, group, *, glob=True):
        flag = "--glob" if glob else "--no-glob"
        opts = cmdoptions.process_options(["--no-progress", flag])
        if opts.glob:
            file_patterns = group.files.get_patterns(TallyTracker())
        else:
            file_patterns = group.files.sorted_members()
        return Dep5Renderer().file_block(group, opts, file_patterns)

    def _file(self, name, parent=None):
        return nodes.FileInfo(parent=parent, name=name)

    def _group_with_file(self, filename, license_name=None):
        group = dep5.Group()
        group.files.add_file(self._file(filename))
        if license_name:
            group.licenses[license_name] = core_types.License.get(license_name)
        return group

    # valid

    def test_valid_empty_group(self):
        self.assertFalse(dep5.Group().copyright_block_valid())

    def test_valid_single_unnamed_file(self):
        group = dep5.Group()
        group.files.add_file(self._file(""))
        self.assertFalse(group.copyright_block_valid())

    def test_valid_single_named_file(self):
        group = dep5.Group()
        group.files.add_file(self._file("src/foo.c"))
        self.assertTrue(group.copyright_block_valid())

    def test_valid_multiple_files(self):
        group = dep5.Group()
        group.files.add_file(self._file("a.c"))
        group.files.add_file(self._file("b.c"))
        self.assertTrue(group.copyright_block_valid())

    # returns "" for invalid

    def test_block_empty_group_returns_empty(self):
        self.assertEqual(self._block(dep5.Group()), "")

    def test_block_single_unnamed_file_returns_empty(self):
        group = dep5.Group()
        group.files.add_file(self._file(""))
        self.assertEqual(self._block(group), "")

    # Files + License, no Copyright

    def test_block_omits_copyright_line_when_no_holders(self):
        block = self._block(self._group_with_file("src/foo.c", "MIT"))
        self.assertEqual(block, "Files: src/foo.c\nLicense: MIT")
        self.assertNotIn("Copyright:", block)

    # full block, all fields

    def test_block_copyright_line_present_with_one_holder(self):
        group = self._group_with_file("src/foo.c", "MIT")
        group.add_copyrights(
            [
                core_types.CopyrightHolder(
                    "Alice",
                    "alice@example.com",
                    core_types.YearRange(2020, 2020),
                ),
            ],
        )
        self.assertEqual(
            self._block(group),
            "Files: src/foo.c\n"
            "Copyright: 2020, Alice <alice@example.com>\n"
            "License: MIT",
        )

    def test_block_comment_line_present_when_set(self):
        group = self._group_with_file("src/foo.c", "MIT")
        group.add_copyrights(
            [
                core_types.CopyrightHolder(
                    "Alice",
                    "alice@example.com",
                    core_types.YearRange(2020, 2020),
                ),
            ],
        )
        group.comments.append("see also COPYING")
        self.assertEqual(
            self._block(group),
            "Files: src/foo.c\n"
            "Copyright: 2020, Alice <alice@example.com>\n"
            "License: MIT\n"
            "Comment: see also COPYING",
        )

    def test_block_omits_comment_line_when_empty(self):
        block = self._block(self._group_with_file("src/foo.c", "MIT"))
        self.assertNotIn("Comment:", block)

    # continuation indentation

    def test_block_multiple_files_indented_by_7_spaces(self):
        group = dep5.Group()
        for name in ["src/bar.c", "src/foo.c"]:
            group.files.add_file(self._file(name))
        group.licenses["MIT"] = core_types.License.get("MIT")
        self.assertEqual(
            self._block(group, glob=False),
            "Files: src/bar.c\n       src/foo.c\nLicense: MIT",
        )

    def test_block_multiple_holders_indented_by_11_spaces(self):
        group = self._group_with_file("src/foo.c", "MIT")
        group.add_copyrights(
            [
                core_types.CopyrightHolder(
                    "Alice",
                    "alice@example.com",
                    core_types.YearRange(2020, 2020),
                ),
                core_types.CopyrightHolder(
                    "Bob",
                    "bob@example.com",
                    core_types.YearRange(2021, 2021),
                ),
            ],
        )
        self.assertEqual(
            self._block(group),
            "Files: src/foo.c\n"
            "Copyright: 2020, Alice <alice@example.com>\n"
            "           2021, Bob <bob@example.com>\n"
            "License: MIT",
        )

    # glob_=True vs glob_=False routing

    def test_block_glob_produces_directory_wildcard(self):
        src = nodes.DirInfo(parent=None, name="src")
        group = dep5.Group()
        for name in ["a.c", "b.c"]:
            group.files.add_file(self._file(name, parent=src))
        group.licenses["MIT"] = core_types.License.get("MIT")
        self.assertEqual(
            self._block(group),
            "Files: src/*\nLicense: MIT",
        )

    def test_block_no_glob_lists_individual_filenames(self):
        src = nodes.DirInfo(parent=None, name="src")
        group = dep5.Group()
        for name in ["b.c", "a.c"]:
            group.files.add_file(self._file(name, parent=src))
        group.licenses["MIT"] = core_types.License.get("MIT")
        self.assertEqual(
            self._block(group, glob=False),
            "Files: src/a.c\n       src/b.c\nLicense: MIT",
        )

    # get_inherited_comment

    def test_inherited_comment_no_filenames_returns_none(self):
        group = dep5.Group()
        self.assertIsNone(group.get_inherited_comment())

    def test_inherited_comment_with_stored_returns_none(self):
        group = dep5.Group()
        group.license_filenames.add("LICENSE")
        group.stored = dc.FilesParagraph.create(["*"], "2020 Alice", dc.License("MIT"))
        self.assertIsNone(group.get_inherited_comment())

    def test_stored_accepts_files_paragraph(self):
        group = dep5.Group()
        paragraph = dc.FilesParagraph.create(["*"], "2020 Alice", dc.License("MIT"))
        group.stored = paragraph
        self.assertIsInstance(group.stored, dc.FilesParagraph)

    def test_inherited_comment_with_filenames_no_stored(self):
        group = dep5.Group()
        group.license_filenames.add("LICENSE")
        comment = group.get_inherited_comment()
        assert comment is not None
        self.assertIn("LICENSE", comment)
        self.assertIn("No explicit license found", comment)

    def test_inherited_comment_multiple_filenames_sorted(self):
        group = dep5.Group()
        group.license_filenames.update(["COPYING", "LICENSE", "AUTHORS"])
        comment = group.get_inherited_comment()
        assert comment is not None
        lines = comment.splitlines()
        # First line is the header; remaining lines contain sorted filenames
        self.assertIn("No explicit license found", lines[0])
        filenames_in_comment = [line.strip() for line in lines[1:]]
        self.assertEqual(filenames_in_comment, ["AUTHORS", "COPYING", "LICENSE"])


class TestGroupSortKey(unittest.TestCase):
    """Unit tests for Group.sort_key() output ordering.

    sort_key() returns a tuple used by an ascending sort in output.py.
    A smaller sort_key means the group appears earlier (higher) in the output.

    Structure: (debian_group, -position, -n_files, -n_copyrights, group_key)
      debian_group=0  normal groups         → first in output
      debian_group=1  UNKNOWN* licenses     → middle
      debian_group=2  debian/ when split    → last in output
    """

    def setUp(self):
        self._saved_licenses = core_types.License.licenses
        core_types.License.licenses = {}
        self.root = nodes.DirInfo(parent=None, name="")
        self.debian_dir = nodes.DirInfo(parent=self.root, name="debian")
        self.src_dir = nodes.DirInfo(parent=self.root, name="src")

    def tearDown(self):
        core_types.License.licenses = self._saved_licenses

    def _opts(self, *, split_debian=True):
        flags = ["--no-progress", "--root", "tests/testdata/one"]
        if not split_debian:
            flags.append("--no-split-debian")
        return cmdoptions.process_options(flags)

    def _group(self, license_name=None, position=0, parent=None, n_files=1):
        """Create a Group with n_files under the given DirInfo parent."""
        group = dep5.Group(position=position)
        parent = parent if parent is not None else self.root
        for i in range(n_files):
            group.files.add_file(nodes.FileInfo(parent=parent, name=f"f{i}.txt"))
        if license_name:
            group.licenses[license_name] = core_types.License.get(license_name)
        return group

    # UNKNOWN/UNKNOWN_COPYRIGHTED

    def test_unknown_license_sorts_after_real_license(self):
        opts = self._opts()
        real = self._group("MIT")
        unknown = self._group()
        self.assertEqual(unknown.license, core_types.UNKNOWN)
        self.assertGreater(unknown.sort_key(opts), real.sort_key(opts))

    def test_unknown_copyrighted_sorts_after_real_license(self):
        opts = self._opts()
        real = self._group("MIT")
        unk_cop = self._group()
        unk_cop.add_copyrights([core_types.CopyrightHolder("", "x@example.com")])
        self.assertEqual(unk_cop.license, core_types.UNKNOWN_COPYRIGHTED)
        self.assertGreater(unk_cop.sort_key(opts), real.sort_key(opts))

    def test_unknown_and_unknown_copyrighted_share_same_band(self):
        opts = self._opts()
        unknown = self._group()
        unk_cop = self._group()
        unk_cop.add_copyrights([core_types.CopyrightHolder("", "x@example.com")])
        # Both land in debian_group=1
        self.assertEqual(unknown.sort_key(opts)[0], 1)
        self.assertEqual(unk_cop.sort_key(opts)[0], 1)

    # split_debian flag

    def test_debian_group_sorts_last_with_split_debian(self):
        opts = self._opts(split_debian=True)
        debian = self._group("MIT", parent=self.debian_dir)
        normal = self._group("MIT", parent=self.src_dir)
        self.assertGreater(debian.sort_key(opts), normal.sort_key(opts))

    def test_debian_group_sorts_after_unknown_with_split_debian(self):
        opts = self._opts(split_debian=True)
        debian = self._group("MIT", parent=self.debian_dir)
        unknown = self._group(parent=self.src_dir)
        # Full three-way ordering: normal(0) < UNKNOWN(1) < debian(2)
        self.assertGreater(debian.sort_key(opts), unknown.sort_key(opts))

    def test_debian_group_not_special_without_split_debian(self):
        opts = self._opts(split_debian=False)
        debian = self._group("MIT", parent=self.debian_dir)
        normal = self._group("MIT", parent=self.src_dir)
        # Both land in debian_group=0 when the flag is off
        self.assertEqual(debian.sort_key(opts)[0], 0)
        self.assertEqual(normal.sort_key(opts)[0], 0)

    # position-based secondary sort

    def test_higher_position_sorts_before_lower_position(self):
        opts = self._opts()
        low = self._group("MIT", position=0)
        high = self._group("MIT", position=3)
        # -3 < 0 → high-position group has smaller sort_key → appears first
        self.assertLess(high.sort_key(opts), low.sort_key(opts))

    def test_position_ordering_across_multiple_groups(self):
        opts = self._opts()
        groups = [self._group("MIT", position=p) for p in [0, 2, 1]]
        ordered = sorted(groups, key=lambda g: g.sort_key(opts))
        self.assertEqual([g.position for g in ordered], [2, 1, 0])

    # file-count secondary sort

    def test_more_files_sorts_before_fewer_files(self):
        opts = self._opts()
        few = self._group("MIT", n_files=1)
        many = self._group("MIT", n_files=3)
        # -3 < -1 → larger group has smaller sort_key → appears first
        self.assertLess(many.sort_key(opts), few.sort_key(opts))

    # full priority end-to-end sort

    def test_full_sort_order_normal_then_unknown_then_debian(self):
        opts = self._opts(split_debian=True)
        g_real = self._group("MIT", position=0, parent=self.src_dir)
        g_unk = self._group(position=0, parent=self.src_dir)
        g_deb = self._group("MIT", position=0, parent=self.debian_dir)

        ordered = sorted([g_deb, g_unk, g_real], key=lambda g: g.sort_key(opts))

        self.assertIs(ordered[0], g_real)
        self.assertIs(ordered[1], g_unk)
        self.assertIs(ordered[2], g_deb)


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