"""Provides unit tests to verify that the Breadth First Search algorithm is functioning correctly."""

import unittest
from collections import defaultdict

from ..pygraph import UndirectedGraph, depth_first_search, get_connected_components
from . import utility_functions


class DepthFirstSearchTest(unittest.TestCase):

    def test_dfs_with_empty_graph(self):
        """Does the ''depth_first_search'' function return an empty list for an empty graph?"""
        graph = UndirectedGraph()

        expected = []
        ordering = depth_first_search(graph)

        self.assertEqual(expected, ordering)

    def test_dfs_with_single_node(self):
        """Does the ''depth_first_search'' function return a single node when given a trivial graph?"""
        graph = utility_functions.build_single_node_graph()

        expected = [1]
        ordering = depth_first_search(graph)

        self.assertEqual(expected, ordering)

    def test_dfs_with_line_graph(self):
        """Does the ''depth_first_search'' function return the proper path for a line-graph?"""
        graph = utility_functions.build_3_node_line_graph()

        expected = [1, 2, 3]
        ordering = depth_first_search(graph, 1)

        self.assertEqual(expected, ordering)

    def test_dfs_depth_ordering(self):
        """Does the ''depth_first_search'' function return nodes in a proper ordering?"""
        graph = utility_functions.build_biconnected_test_graph()

        ordering = depth_first_search(graph, 1)
        node_lookup_by_index = dict(list(zip(list(range(1, len(ordering) + 1)), ordering)))
        items_sorted_by_dfs_index = list(zip(ordering, list(range(1, len(ordering) + 1))))
        # index_lookup_by_node = dict(items_sorted_by_dfs_index)

        visited_by_node = defaultdict(lambda: False)

        connected_components = get_connected_components(graph)

        # If a node and its dfs-list successor are in the same connected component, and the non-successor node has
        # unvisited neighbors, then the successor must be one of those neighbors

        def in_same_component(node_a, node_b):
            for component in connected_components:
                if node_a in component:
                    return node_b in component
            return False

        for node_id, dfs_index in items_sorted_by_dfs_index[:-1]:
            visited_by_node[node_id] = True
            successor_node_id = node_lookup_by_index[dfs_index+1]
            if in_same_component(node_id, successor_node_id):
                neighbor_nodes = graph.neighbors(node_id)
                has_unvisited_neighbors = any([not visited_by_node[n] for n in neighbor_nodes])
                if has_unvisited_neighbors:
                    self.assertIn(successor_node_id, neighbor_nodes)

    def test_dfs_with_connected_graph_contains_all_nodes(self):
        """Does the ''depth_first_search'' function return all the nodes for a connected graph?"""
        graph = utility_functions.build_biconnected_test_graph()

        all_nodes = graph.get_all_node_ids()
        ordering = depth_first_search(graph, 1)

        self.assertEqual(len(all_nodes), len(ordering))

        for n in all_nodes:
            self.assertIn(n, ordering)

    def test_dfs_with_disconnected_graph_contains_all_nodes(self):
        """Does the ''depth_first_search'' function return all the nodes for a disconnected graph?"""
        graph = utility_functions.build_disconnected_test_graph()

        all_nodes = graph.get_all_node_ids()
        ordering = depth_first_search(graph, 1)

        self.assertEqual(len(all_nodes), len(ordering))

        for n in all_nodes:
            self.assertIn(n, ordering)

