from itertools import chain, islice, tee
from random import shuffle

import pytest

import networkx
import networkx as nx
from networkx.algorithms import find_cycle, minimum_cycle_basis
from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE


class TestCycles:
    @classmethod
    def setup_class(cls):
        G = networkx.Graph()
        nx.add_cycle(G, [0, 1, 2, 3])
        nx.add_cycle(G, [0, 3, 4, 5])
        nx.add_cycle(G, [0, 1, 6, 7, 8])
        G.add_edge(8, 9)
        cls.G = G

    def is_cyclic_permutation(self, a, b):
        n = len(a)
        if len(b) != n:
            return False
        l = a + a
        return any(l[i : i + n] == b for i in range(n))

    def test_cycle_basis(self):
        G = self.G
        cy = networkx.cycle_basis(G, 0)
        sort_cy = sorted(sorted(c) for c in cy)
        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
        cy = networkx.cycle_basis(G, 1)
        sort_cy = sorted(sorted(c) for c in cy)
        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
        cy = networkx.cycle_basis(G, 9)
        sort_cy = sorted(sorted(c) for c in cy)
        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
        # test disconnected graphs
        nx.add_cycle(G, "ABC")
        cy = networkx.cycle_basis(G, 9)
        sort_cy = sorted(sorted(c) for c in cy[:-1]) + [sorted(cy[-1])]
        assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5], ["A", "B", "C"]]

    def test_cycle_basis2(self):
        with pytest.raises(nx.NetworkXNotImplemented):
            G = nx.DiGraph()
            cy = networkx.cycle_basis(G, 0)

    def test_cycle_basis3(self):
        with pytest.raises(nx.NetworkXNotImplemented):
            G = nx.MultiGraph()
            cy = networkx.cycle_basis(G, 0)

    def test_cycle_basis_self_loop(self):
        """Tests the function for graphs with self loops"""
        G = nx.Graph()
        nx.add_cycle(G, [0, 1, 2, 3])
        nx.add_cycle(G, [0, 0, 6, 2])
        cy = nx.cycle_basis(G)
        sort_cy = sorted(sorted(c) for c in cy)
        assert sort_cy == [[0], [0, 1, 2], [0, 2, 3], [0, 2, 6]]

    def test_simple_cycles(self):
        edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
        G = nx.DiGraph(edges)
        cc = sorted(nx.simple_cycles(G))
        ca = [[0], [0, 1, 2], [0, 2], [1, 2], [2]]
        assert len(cc) == len(ca)
        for c in cc:
            assert any(self.is_cyclic_permutation(c, rc) for rc in ca)

    def test_unsortable(self):
        # this test ensures that graphs whose nodes without an intrinsic
        # ordering do not cause issues
        G = nx.DiGraph()
        nx.add_cycle(G, ["a", 1])
        c = list(nx.simple_cycles(G))
        assert len(c) == 1

    def test_simple_cycles_small(self):
        G = nx.DiGraph()
        nx.add_cycle(G, [1, 2, 3])
        c = sorted(nx.simple_cycles(G))
        assert len(c) == 1
        assert self.is_cyclic_permutation(c[0], [1, 2, 3])
        nx.add_cycle(G, [10, 20, 30])
        cc = sorted(nx.simple_cycles(G))
        assert len(cc) == 2
        ca = [[1, 2, 3], [10, 20, 30]]
        for c in cc:
            assert any(self.is_cyclic_permutation(c, rc) for rc in ca)

    def test_simple_cycles_empty(self):
        G = nx.DiGraph()
        assert list(nx.simple_cycles(G)) == []

    def worst_case_graph(self, k):
        # see figure 1 in Johnson's paper
        # this graph has exactly 3k simple cycles
        G = nx.DiGraph()
        for n in range(2, k + 2):
            G.add_edge(1, n)
            G.add_edge(n, k + 2)
        G.add_edge(2 * k + 1, 1)
        for n in range(k + 2, 2 * k + 2):
            G.add_edge(n, 2 * k + 2)
            G.add_edge(n, n + 1)
        G.add_edge(2 * k + 3, k + 2)
        for n in range(2 * k + 3, 3 * k + 3):
            G.add_edge(2 * k + 2, n)
            G.add_edge(n, 3 * k + 3)
        G.add_edge(3 * k + 3, 2 * k + 2)
        return G

    def test_worst_case_graph(self):
        # see figure 1 in Johnson's paper
        for k in range(3, 10):
            G = self.worst_case_graph(k)
            l = len(list(nx.simple_cycles(G)))
            assert l == 3 * k

    def test_recursive_simple_and_not(self):
        for k in range(2, 10):
            G = self.worst_case_graph(k)
            cc = sorted(nx.simple_cycles(G))
            rcc = sorted(nx.recursive_simple_cycles(G))
            assert len(cc) == len(rcc)
            for c in cc:
                assert any(self.is_cyclic_permutation(c, r) for r in rcc)
            for rc in rcc:
                assert any(self.is_cyclic_permutation(rc, c) for c in cc)

    def test_simple_graph_with_reported_bug(self):
        G = nx.DiGraph()
        edges = [
            (0, 2),
            (0, 3),
            (1, 0),
            (1, 3),
            (2, 1),
            (2, 4),
            (3, 2),
            (3, 4),
            (4, 0),
            (4, 1),
            (4, 5),
            (5, 0),
            (5, 1),
            (5, 2),
            (5, 3),
        ]
        G.add_edges_from(edges)
        cc = sorted(nx.simple_cycles(G))
        assert len(cc) == 26
        rcc = sorted(nx.recursive_simple_cycles(G))
        assert len(cc) == len(rcc)
        for c in cc:
            assert any(self.is_cyclic_permutation(c, rc) for rc in rcc)
        for rc in rcc:
            assert any(self.is_cyclic_permutation(rc, c) for c in cc)


def pairwise(iterable):
    a, b = tee(iterable)
    next(b, None)
    return zip(a, b)


def cycle_edges(c):
    return pairwise(chain(c, islice(c, 1)))


def directed_cycle_edgeset(c):
    return frozenset(cycle_edges(c))


def undirected_cycle_edgeset(c):
    if len(c) == 1:
        return frozenset(cycle_edges(c))
    return frozenset(map(frozenset, cycle_edges(c)))


def multigraph_cycle_edgeset(c):
    if len(c) <= 2:
        return frozenset(cycle_edges(c))
    else:
        return frozenset(map(frozenset, cycle_edges(c)))


class TestCycleEnumeration:
    @staticmethod
    def K(n):
        return nx.complete_graph(n)

    @staticmethod
    def D(n):
        return nx.complete_graph(n).to_directed()

    @staticmethod
    def edgeset_function(g):
        if g.is_directed():
            return directed_cycle_edgeset
        elif g.is_multigraph():
            return multigraph_cycle_edgeset
        else:
            return undirected_cycle_edgeset

    def check_cycle(self, g, c, es, cache, source, original_c, length_bound, chordless):
        if length_bound is not None and len(c) > length_bound:
            raise RuntimeError(
                f"computed cycle {original_c} exceeds length bound {length_bound}"
            )
        if source == "computed":
            if es in cache:
                raise RuntimeError(
                    f"computed cycle {original_c} has already been found!"
                )
            else:
                cache[es] = tuple(original_c)
        else:
            if es in cache:
                cache.pop(es)
            else:
                raise RuntimeError(f"expected cycle {original_c} was not computed")

        if not all(g.has_edge(*e) for e in es):
            raise RuntimeError(
                f"{source} claimed cycle {original_c} is not a cycle of g"
            )
        if chordless and len(g.subgraph(c).edges) > len(c):
            raise RuntimeError(f"{source} cycle {original_c} is not chordless")

    def check_cycle_algorithm(
        self,
        g,
        expected_cycles,
        length_bound=None,
        chordless=False,
        algorithm=None,
    ):
        if algorithm is None:
            algorithm = nx.chordless_cycles if chordless else nx.simple_cycles

        # note: we shuffle the labels of g to rule out accidentally-correct
        # behavior which occurred during the development of chordless cycle
        # enumeration algorithms

        relabel = list(range(len(g)))
        shuffle(relabel)
        label = dict(zip(g, relabel))
        unlabel = dict(zip(relabel, g))
        h = nx.relabel_nodes(g, label, copy=True)

        edgeset = self.edgeset_function(h)

        params = {}
        if length_bound is not None:
            params["length_bound"] = length_bound

        cycle_cache = {}
        for c in algorithm(h, **params):
            original_c = [unlabel[x] for x in c]
            es = edgeset(c)
            self.check_cycle(
                h, c, es, cycle_cache, "computed", original_c, length_bound, chordless
            )

        if isinstance(expected_cycles, int):
            if len(cycle_cache) != expected_cycles:
                raise RuntimeError(
                    f"expected {expected_cycles} cycles, got {len(cycle_cache)}"
                )
            return
        for original_c in expected_cycles:
            c = [label[x] for x in original_c]
            es = edgeset(c)
            self.check_cycle(
                h, c, es, cycle_cache, "expected", original_c, length_bound, chordless
            )

        if len(cycle_cache):
            for c in cycle_cache.values():
                raise RuntimeError(
                    f"computed cycle {c} is valid but not in the expected cycle set!"
                )

    def check_cycle_enumeration_integer_sequence(
        self,
        g_family,
        cycle_counts,
        length_bound=None,
        chordless=False,
        algorithm=None,
    ):
        for g, num_cycles in zip(g_family, cycle_counts):
            self.check_cycle_algorithm(
                g,
                num_cycles,
                length_bound=length_bound,
                chordless=chordless,
                algorithm=algorithm,
            )

    def test_directed_chordless_cycle_digons(self):
        g = nx.DiGraph()
        nx.add_cycle(g, range(5))
        nx.add_cycle(g, range(5)[::-1])
        g.add_edge(0, 0)
        expected_cycles = [(0,), (1, 2), (2, 3), (3, 4)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

        self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=2)

        expected_cycles = [c for c in expected_cycles if len(c) < 2]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=1)

    def test_directed_chordless_cycle_undirected(self):
        g = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (5, 1), (0, 2)])
        expected_cycles = [(0, 2, 3, 4, 5), (1, 2, 3, 4, 5)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

        g = nx.DiGraph()
        nx.add_cycle(g, range(5))
        nx.add_cycle(g, range(4, 9))
        g.add_edge(7, 3)
        expected_cycles = [(0, 1, 2, 3, 4), (3, 4, 5, 6, 7), (4, 5, 6, 7, 8)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

        g.add_edge(3, 7)
        expected_cycles = [(0, 1, 2, 3, 4), (3, 7), (4, 5, 6, 7, 8)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

        expected_cycles = [(3, 7)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=4)

        g.remove_edge(7, 3)
        expected_cycles = [(0, 1, 2, 3, 4), (4, 5, 6, 7, 8)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

        g = nx.DiGraph((i, j) for i in range(10) for j in range(i))
        expected_cycles = []
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

    def test_chordless_cycles_directed(self):
        G = nx.DiGraph()
        nx.add_cycle(G, range(5))
        nx.add_cycle(G, range(4, 12))
        expected = [[*range(5)], [*range(4, 12)]]
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
        )

        G.add_edge(7, 3)
        expected.append([*range(3, 8)])
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
        )

        G.add_edge(3, 7)
        expected[-1] = [7, 3]
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
        )

        expected.pop()
        G.remove_edge(7, 3)
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
        )

    def test_directed_chordless_cycle_diclique(self):
        g_family = [self.D(n) for n in range(10)]
        expected_cycles = [(n * n - n) // 2 for n in range(10)]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected_cycles, chordless=True
        )

        expected_cycles = [(n * n - n) // 2 for n in range(10)]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected_cycles, length_bound=2
        )

    def test_directed_chordless_loop_blockade(self):
        g = nx.DiGraph((i, i) for i in range(10))
        nx.add_cycle(g, range(10))
        expected_cycles = [(i,) for i in range(10)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

        self.check_cycle_algorithm(g, expected_cycles, length_bound=1)

        g = nx.MultiDiGraph(g)
        g.add_edges_from((i, i) for i in range(0, 10, 2))
        expected_cycles = [(i,) for i in range(1, 10, 2)]
        self.check_cycle_algorithm(g, expected_cycles, chordless=True)

    def test_simple_cycles_notable_clique_sequences(self):
        # A000292: Number of labeled graphs on n+3 nodes that are triangles.
        g_family = [self.K(n) for n in range(2, 12)]
        expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected, length_bound=3
        )

        def triangles(g, **kwargs):
            yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 3)

        # directed complete graphs have twice as many triangles thanks to reversal
        g_family = [self.D(n) for n in range(2, 12)]
        expected = [2 * e for e in expected]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected, length_bound=3, algorithm=triangles
        )

        def four_cycles(g, **kwargs):
            yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 4)

        # A050534: the number of 4-cycles in the complete graph K_{n+1}
        expected = [0, 0, 0, 3, 15, 45, 105, 210, 378, 630, 990]
        g_family = [self.K(n) for n in range(1, 12)]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected, length_bound=4, algorithm=four_cycles
        )

        # directed complete graphs have twice as many 4-cycles thanks to reversal
        expected = [2 * e for e in expected]
        g_family = [self.D(n) for n in range(1, 15)]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected, length_bound=4, algorithm=four_cycles
        )

        # A006231: the number of elementary circuits in a complete directed graph with n nodes
        expected = [0, 1, 5, 20, 84, 409, 2365]
        g_family = [self.D(n) for n in range(1, 8)]
        self.check_cycle_enumeration_integer_sequence(g_family, expected)

        # A002807: Number of cycles in the complete graph on n nodes K_{n}.
        expected = [0, 0, 0, 1, 7, 37, 197, 1172]
        g_family = [self.K(n) for n in range(8)]
        self.check_cycle_enumeration_integer_sequence(g_family, expected)

    def test_directed_chordless_cycle_parallel_multiedges(self):
        g = nx.MultiGraph()

        nx.add_cycle(g, range(5))
        expected = [[*range(5)]]
        self.check_cycle_algorithm(g, expected, chordless=True)

        nx.add_cycle(g, range(5))
        expected = [*cycle_edges(range(5))]
        self.check_cycle_algorithm(g, expected, chordless=True)

        nx.add_cycle(g, range(5))
        expected = []
        self.check_cycle_algorithm(g, expected, chordless=True)

        g = nx.MultiDiGraph()

        nx.add_cycle(g, range(5))
        expected = [[*range(5)]]
        self.check_cycle_algorithm(g, expected, chordless=True)

        nx.add_cycle(g, range(5))
        self.check_cycle_algorithm(g, [], chordless=True)

        nx.add_cycle(g, range(5))
        self.check_cycle_algorithm(g, [], chordless=True)

        g = nx.MultiDiGraph()

        nx.add_cycle(g, range(5))
        nx.add_cycle(g, range(5)[::-1])
        expected = [*cycle_edges(range(5))]
        self.check_cycle_algorithm(g, expected, chordless=True)

        nx.add_cycle(g, range(5))
        self.check_cycle_algorithm(g, [], chordless=True)

    def test_chordless_cycles_graph(self):
        G = nx.Graph()
        nx.add_cycle(G, range(5))
        nx.add_cycle(G, range(4, 12))
        expected = [[*range(5)], [*range(4, 12)]]
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
        )

        G.add_edge(7, 3)
        expected.append([*range(3, 8)])
        expected.append([4, 3, 7, 8, 9, 10, 11])
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
        )

    def test_chordless_cycles_giant_hamiltonian(self):
        # ... o - e - o - e - o ... # o = odd, e = even
        # ... ---/ \-----/ \--- ... # <-- "long" edges
        #
        # each long edge belongs to exactly one triangle, and one giant cycle
        # of length n/2.  The remaining edges each belong to a triangle

        n = 1000
        assert n % 2 == 0
        G = nx.Graph()
        for v in range(n):
            if not v % 2:
                G.add_edge(v, (v + 2) % n)
            G.add_edge(v, (v + 1) % n)

        expected = [[*range(0, n, 2)]] + [
            [x % n for x in range(i, i + 3)] for i in range(0, n, 2)
        ]
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
        )

        # ... o -> e -> o -> e -> o ... # o = odd, e = even
        # ... <---/ \---<---/ \---< ... # <-- "long" edges
        #
        # this time, we orient the short and long edges in opposition
        # the cycle structure of this graph is the same, but we need to reverse
        # the long one in our representation.  Also, we need to drop the size
        # because our partitioning algorithm uses strongly connected components
        # instead of separating graphs by their strong articulation points

        n = 100
        assert n % 2 == 0
        G = nx.DiGraph()
        for v in range(n):
            G.add_edge(v, (v + 1) % n)
            if not v % 2:
                G.add_edge((v + 2) % n, v)

        expected = [[*range(n - 2, -2, -2)]] + [
            [x % n for x in range(i, i + 3)] for i in range(0, n, 2)
        ]
        self.check_cycle_algorithm(G, expected, chordless=True)
        self.check_cycle_algorithm(
            G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
        )

    def test_simple_cycles_acyclic_tournament(self):
        n = 10
        G = nx.DiGraph((x, y) for x in range(n) for y in range(x))
        self.check_cycle_algorithm(G, [])
        self.check_cycle_algorithm(G, [], chordless=True)

        for k in range(n + 1):
            self.check_cycle_algorithm(G, [], length_bound=k)
            self.check_cycle_algorithm(G, [], length_bound=k, chordless=True)

    def test_simple_cycles_graph(self):
        testG = nx.cycle_graph(8)
        cyc1 = tuple(range(8))
        self.check_cycle_algorithm(testG, [cyc1])

        testG.add_edge(4, -1)
        nx.add_path(testG, [3, -2, -3, -4])
        self.check_cycle_algorithm(testG, [cyc1])

        testG.update(nx.cycle_graph(range(8, 16)))
        cyc2 = tuple(range(8, 16))
        self.check_cycle_algorithm(testG, [cyc1, cyc2])

        testG.update(nx.cycle_graph(range(4, 12)))
        cyc3 = tuple(range(4, 12))
        expected = {
            (0, 1, 2, 3, 4, 5, 6, 7),  # cyc1
            (8, 9, 10, 11, 12, 13, 14, 15),  # cyc2
            (4, 5, 6, 7, 8, 9, 10, 11),  # cyc3
            (4, 5, 6, 7, 8, 15, 14, 13, 12, 11),  # cyc2 + cyc3
            (0, 1, 2, 3, 4, 11, 10, 9, 8, 7),  # cyc1 + cyc3
            (0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7),  # cyc1 + cyc2 + cyc3
        }
        self.check_cycle_algorithm(testG, expected)
        assert len(expected) == (2**3 - 1) - 1  # 1 disjoint comb: cyc1 + cyc2

        # Basis size = 5 (2 loops overlapping gives 5 small loops
        #        E
        #       / \         Note: A-F = 10-15
        #    1-2-3-4-5
        #    / |   |  \   cyc1=012DAB -- left
        #   0  D   F  6   cyc2=234E   -- top
        #   \  |   |  /   cyc3=45678F -- right
        #    B-A-9-8-7    cyc4=89AC   -- bottom
        #       \ /       cyc5=234F89AD -- middle
        #        C
        #
        # combinations of 5 basis elements: 2^5 - 1  (one includes no cycles)
        #
        # disjoint combs: (11 total) not simple cycles
        #   Any pair not including cyc5 => choose(4, 2) = 6
        #   Any triple not including cyc5 => choose(4, 3) = 4
        #   Any quad not including cyc5 => choose(4, 4) = 1
        #
        # we expect 31 - 11 = 20 simple cycles
        #
        testG = nx.cycle_graph(12)
        testG.update(nx.cycle_graph([12, 10, 13, 2, 14, 4, 15, 8]).edges)
        expected = (2**5 - 1) - 11  # 11 disjoint combinations
        self.check_cycle_algorithm(testG, expected)

    def test_simple_cycles_bounded(self):
        # iteratively construct a cluster of nested cycles running in the same direction
        # there should be one cycle of every length
        d = nx.DiGraph()
        expected = []
        for n in range(10):
            nx.add_cycle(d, range(n))
            expected.append(n)
            for k, e in enumerate(expected):
                self.check_cycle_algorithm(d, e, length_bound=k)

        # iteratively construct a path of undirected cycles, connected at articulation
        # points.  there should be one cycle of every length except 2: no digons
        g = nx.Graph()
        top = 0
        expected = []
        for n in range(10):
            expected.append(n if n < 2 else n - 1)
            if n == 2:
                # no digons in undirected graphs
                continue
            nx.add_cycle(g, range(top, top + n))
            top += n
            for k, e in enumerate(expected):
                self.check_cycle_algorithm(g, e, length_bound=k)

    def test_simple_cycles_bound_corner_cases(self):
        G = nx.cycle_graph(4)
        DG = nx.cycle_graph(4, create_using=nx.DiGraph)
        assert list(nx.simple_cycles(G, length_bound=0)) == []
        assert list(nx.simple_cycles(DG, length_bound=0)) == []
        assert list(nx.chordless_cycles(G, length_bound=0)) == []
        assert list(nx.chordless_cycles(DG, length_bound=0)) == []

    def test_simple_cycles_bound_error(self):
        with pytest.raises(ValueError):
            G = nx.DiGraph()
            for c in nx.simple_cycles(G, -1):
                assert False

        with pytest.raises(ValueError):
            G = nx.Graph()
            for c in nx.simple_cycles(G, -1):
                assert False

        with pytest.raises(ValueError):
            G = nx.Graph()
            for c in nx.chordless_cycles(G, -1):
                assert False

        with pytest.raises(ValueError):
            G = nx.DiGraph()
            for c in nx.chordless_cycles(G, -1):
                assert False

    def test_chordless_cycles_clique(self):
        g_family = [self.K(n) for n in range(2, 15)]
        expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 286, 364]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected, chordless=True
        )

        # directed cliques have as many digons as undirected graphs have edges
        expected = [(n * n - n) // 2 for n in range(15)]
        g_family = [self.D(n) for n in range(15)]
        self.check_cycle_enumeration_integer_sequence(
            g_family, expected, chordless=True
        )


# These tests might fail with hash randomization since they depend on
# edge_dfs. For more information, see the comments in:
#    networkx/algorithms/traversal/tests/test_edgedfs.py


class TestFindCycle:
    @classmethod
    def setup_class(cls):
        cls.nodes = [0, 1, 2, 3]
        cls.edges = [(-1, 0), (0, 1), (1, 0), (1, 0), (2, 1), (3, 1)]

    def test_graph_nocycle(self):
        G = nx.Graph(self.edges)
        pytest.raises(nx.exception.NetworkXNoCycle, find_cycle, G, self.nodes)

    def test_graph_cycle(self):
        G = nx.Graph(self.edges)
        G.add_edge(2, 0)
        x = list(find_cycle(G, self.nodes))
        x_ = [(0, 1), (1, 2), (2, 0)]
        assert x == x_

    def test_graph_orientation_none(self):
        G = nx.Graph(self.edges)
        G.add_edge(2, 0)
        x = list(find_cycle(G, self.nodes, orientation=None))
        x_ = [(0, 1), (1, 2), (2, 0)]
        assert x == x_

    def test_graph_orientation_original(self):
        G = nx.Graph(self.edges)
        G.add_edge(2, 0)
        x = list(find_cycle(G, self.nodes, orientation="original"))
        x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 0, FORWARD)]
        assert x == x_

    def test_digraph(self):
        G = nx.DiGraph(self.edges)
        x = list(find_cycle(G, self.nodes))
        x_ = [(0, 1), (1, 0)]
        assert x == x_

    def test_digraph_orientation_none(self):
        G = nx.DiGraph(self.edges)
        x = list(find_cycle(G, self.nodes, orientation=None))
        x_ = [(0, 1), (1, 0)]
        assert x == x_

    def test_digraph_orientation_original(self):
        G = nx.DiGraph(self.edges)
        x = list(find_cycle(G, self.nodes, orientation="original"))
        x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
        assert x == x_

    def test_multigraph(self):
        G = nx.MultiGraph(self.edges)
        x = list(find_cycle(G, self.nodes))
        x_ = [(0, 1, 0), (1, 0, 1)]  # or (1, 0, 2)
        # Hash randomization...could be any edge.
        assert x[0] == x_[0]
        assert x[1][:2] == x_[1][:2]

    def test_multidigraph(self):
        G = nx.MultiDiGraph(self.edges)
        x = list(find_cycle(G, self.nodes))
        x_ = [(0, 1, 0), (1, 0, 0)]  # (1, 0, 1)
        assert x[0] == x_[0]
        assert x[1][:2] == x_[1][:2]

    def test_digraph_ignore(self):
        G = nx.DiGraph(self.edges)
        x = list(find_cycle(G, self.nodes, orientation="ignore"))
        x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
        assert x == x_

    def test_digraph_reverse(self):
        G = nx.DiGraph(self.edges)
        x = list(find_cycle(G, self.nodes, orientation="reverse"))
        x_ = [(1, 0, REVERSE), (0, 1, REVERSE)]
        assert x == x_

    def test_multidigraph_ignore(self):
        G = nx.MultiDiGraph(self.edges)
        x = list(find_cycle(G, self.nodes, orientation="ignore"))
        x_ = [(0, 1, 0, FORWARD), (1, 0, 0, FORWARD)]  # or (1, 0, 1, 1)
        assert x[0] == x_[0]
        assert x[1][:2] == x_[1][:2]
        assert x[1][3] == x_[1][3]

    def test_multidigraph_ignore2(self):
        # Loop traversed an edge while ignoring its orientation.
        G = nx.MultiDiGraph([(0, 1), (1, 2), (1, 2)])
        x = list(find_cycle(G, [0, 1, 2], orientation="ignore"))
        x_ = [(1, 2, 0, FORWARD), (1, 2, 1, REVERSE)]
        assert x == x_

    def test_multidigraph_original(self):
        # Node 2 doesn't need to be searched again from visited from 4.
        # The goal here is to cover the case when 2 to be researched from 4,
        # when 4 is visited from the first time (so we must make sure that 4
        # is not visited from 2, and hence, we respect the edge orientation).
        G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 3), (4, 2)])
        pytest.raises(
            nx.exception.NetworkXNoCycle,
            find_cycle,
            G,
            [0, 1, 2, 3, 4],
            orientation="original",
        )

    def test_dag(self):
        G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
        pytest.raises(
            nx.exception.NetworkXNoCycle, find_cycle, G, orientation="original"
        )
        x = list(find_cycle(G, orientation="ignore"))
        assert x == [(0, 1, FORWARD), (1, 2, FORWARD), (0, 2, REVERSE)]

    def test_prev_explored(self):
        # https://github.com/networkx/networkx/issues/2323

        G = nx.DiGraph()
        G.add_edges_from([(1, 0), (2, 0), (1, 2), (2, 1)])
        pytest.raises(nx.NetworkXNoCycle, find_cycle, G, source=0)
        x = list(nx.find_cycle(G, 1))
        x_ = [(1, 2), (2, 1)]
        assert x == x_

        x = list(nx.find_cycle(G, 2))
        x_ = [(2, 1), (1, 2)]
        assert x == x_

        x = list(nx.find_cycle(G))
        x_ = [(1, 2), (2, 1)]
        assert x == x_

    def test_no_cycle(self):
        # https://github.com/networkx/networkx/issues/2439

        G = nx.DiGraph()
        G.add_edges_from([(1, 2), (2, 0), (3, 1), (3, 2)])
        pytest.raises(nx.NetworkXNoCycle, find_cycle, G, source=0)
        pytest.raises(nx.NetworkXNoCycle, find_cycle, G)


def assert_basis_equal(a, b):
    assert sorted(a) == sorted(b)


class TestMinimumCycles:
    @classmethod
    def setup_class(cls):
        T = nx.Graph()
        nx.add_cycle(T, [1, 2, 3, 4], weight=1)
        T.add_edge(2, 4, weight=5)
        cls.diamond_graph = T

    def test_unweighted_diamond(self):
        mcb = minimum_cycle_basis(self.diamond_graph)
        assert_basis_equal([sorted(c) for c in mcb], [[1, 2, 4], [2, 3, 4]])

    def test_weighted_diamond(self):
        mcb = minimum_cycle_basis(self.diamond_graph, weight="weight")
        assert_basis_equal([sorted(c) for c in mcb], [[1, 2, 4], [1, 2, 3, 4]])

    def test_dimensionality(self):
        # checks |MCB|=|E|-|V|+|NC|
        ntrial = 10
        for _ in range(ntrial):
            rg = nx.erdos_renyi_graph(10, 0.3)
            nnodes = rg.number_of_nodes()
            nedges = rg.number_of_edges()
            ncomp = nx.number_connected_components(rg)

            dim_mcb = len(minimum_cycle_basis(rg))
            assert dim_mcb == nedges - nnodes + ncomp

    def test_complete_graph(self):
        cg = nx.complete_graph(5)
        mcb = minimum_cycle_basis(cg)
        assert all(len(cycle) == 3 for cycle in mcb)

    def test_tree_graph(self):
        tg = nx.balanced_tree(3, 3)
        assert not minimum_cycle_basis(tg)
