from unittest.mock import Mock, patch

from networkx import DiGraph, NetworkXNoPath, NodeNotFound

from django.test import SimpleTestCase

from routing.routing import (
    _route_path, route_check_edencom, route_check_triglavian, route_length,
    route_path_gates, route_path_nodes, systems_range,
)


def _make_graph() -> DiGraph:
    graph = DiGraph()
    graph.add_node(1, security=0.5, type="solar_system")
    graph.add_node(2, security=0.4, type="solar_system")
    graph.add_node(3, security=0.3, type="solar_system")
    graph.add_edge(1, 2, p_shortest=1, p_safest=2, p_less_safe=1, type="Stargate")
    graph.add_edge(2, 3, p_shortest=1, p_safest=2, p_less_safe=1, type="Stargate")
    graph.add_edge(1, 3, p_shortest=5, p_safest=5, p_less_safe=5, type="Stargate")
    return graph


class RoutingFunctionsTest(SimpleTestCase):
    @patch("routing.routing.build")
    def test_route_path_nodes(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        self.assertEqual(route_path_nodes(1, 3), [1, 2, 3])

    @patch("routing.routing.build")
    def test_route_path_gates(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        self.assertEqual(route_path_gates(1, 3), [(1, 2, "Stargate"), (2, 3, "Stargate")])

    @patch("routing.routing.build")
    def test_route_length(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        self.assertEqual(route_length(1, 3), 2)

    @patch("routing.routing.build")
    def test_systems_range(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        self.assertEqual(sorted(systems_range(1, 1)), [2])

    @patch("routing.routing.build")
    def test_systems_range_include_source(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        self.assertEqual(sorted(systems_range(1, 1, include_source=True)), [1, 2])

    @patch("routing.routing.route_path_nodes", return_value=[1, 2, 3])
    @patch("routing.routing.TrigInvasion.objects.filter")
    def test_route_check_edencom_current_behavior(self, mock_filter: Mock, _mock_nodes: Mock) -> None:
        mock_filter.return_value.exists = Mock(return_value=True)
        self.assertFalse(route_check_edencom(1, 3))

    @patch("routing.routing.route_path_nodes", return_value=[1, 2, 3])
    @patch("routing.routing.TrigInvasion.objects.filter")
    def test_route_check_triglavian_current_behavior(self, mock_filter: Mock, _mock_nodes: Mock) -> None:
        mock_filter.return_value.exists = Mock(return_value=True)
        self.assertFalse(route_check_triglavian(1, 3))


class RoutingAlgorithmsTest(SimpleTestCase):
    @patch("routing.routing.build")
    def test_astar(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        _graph, nodes = _route_path(1, 3, algorithm="astar")
        self.assertEqual(nodes, [1, 2, 3])

    @patch("routing.routing.build")
    def test_dijkstra(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        _graph, nodes = _route_path(1, 3, algorithm="dijkstra")
        self.assertEqual(nodes, [1, 2, 3])

    @patch("routing.routing.build")
    def test_bellman_ford(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        _graph, nodes = _route_path(1, 3, algorithm="bellman_ford")
        self.assertEqual(nodes, [1, 2, 3])

    @patch("routing.routing.build")
    def test_invalid_algorithm_falls_back_to_astar(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_graph()
        _graph, nodes = _route_path(1, 3, algorithm="unknown")
        self.assertEqual(nodes, [1, 2, 3])


class RoutingIntegrationTest(SimpleTestCase):
    """Integration tests with real static cache data"""

    @patch("routing.routing.build")
    def test_route_modes_return_different_paths(self, mock_build: Mock) -> None:
        """Verify different routing modes can produce different paths"""
        graph = _make_graph()
        mock_build.return_value = graph

        shortest = route_length(1, 3, mode="p_shortest")
        safest = route_length(1, 3, mode="p_safest")
        # Both should be valid
        self.assertGreater(shortest, 0)
        self.assertGreater(safest, 0)

    @patch("routing.routing.build")
    def test_route_gates_includes_gate_type(self, mock_build: Mock) -> None:
        """Verify gate information includes type"""
        mock_build.return_value = _make_graph()
        gates = route_path_gates(1, 3)
        for gate_tuple in gates:
            self.assertEqual(len(gate_tuple), 3)
            self.assertEqual(gate_tuple[2], "Stargate")

    @patch("routing.routing.build")
    def test_systems_range_excludes_source_by_default(self, mock_build: Mock) -> None:
        """Verify source system is excluded from range by default"""
        mock_build.return_value = _make_graph()
        systems = systems_range(1, 2)
        self.assertNotIn(1, systems)

    @patch("routing.routing.build")
    def test_route_length_matches_path_length(self, mock_build: Mock) -> None:
        """Verify route_length equals len(path) - 1"""
        mock_build.return_value = _make_graph()
        path = route_path_nodes(1, 3)
        length = route_length(1, 3)
        self.assertEqual(length, len(path) - 1)


class RoutingEmptyEdgesTest(SimpleTestCase):
    """Verify _route_path skips add_edges_from when edges is empty/falsy"""

    @patch("routing.routing.build")
    def test_empty_edges_does_not_add_edges(self, mock_build: Mock) -> None:
        """Explicit edges=[] should not call add_edges_from (if edges: is False)"""
        graph = _make_graph()
        mock_build.return_value = graph

        with patch.object(graph, "add_edges_from") as mock_add:
            _route_path(1, 3, edges=[])

        mock_add.assert_not_called()

    @patch("routing.routing.build")
    def test_non_empty_edges_calls_add_edges_from(self, mock_build: Mock) -> None:
        """Non-empty edges list should call add_edges_from"""
        graph = _make_graph()
        mock_build.return_value = graph

        extra = [(1, 3, {"p_shortest": 1, "p_safest": 1, "p_less_safe": 1, "type": "Bridge"})]
        with patch.object(graph, "add_edges_from") as mock_add:
            _route_path(1, 3, edges=extra)

        mock_add.assert_called_once_with(extra)


class StaticCacheRegressionTest(SimpleTestCase):
    def test_jita_to_b170_shortest_static_path_and_gates(self) -> None:
        self.assertEqual(
            route_path_nodes(30000142, 30004590, mode="p_shortest", static_cache=True),
            [
                30000142, 30000143, 30001379, 30001376, 30002813, 30045346, 30045345, 30045353,
                30045338, 30045344, 30003837, 30003836, 30003841, 30004046, 30004044, 30004043,
                30004042, 30004040, 30004589, 30004586, 30004584, 30004554, 30004552, 30004553,
                30004555, 30004557, 30004573, 30004574, 30004575, 30004578, 30004581, 30004583,
                30004590,
            ],
        )
        self.assertEqual(
            route_path_gates(30000142, 30004590, mode="p_shortest", static_cache=True),
            [
                (30000142, 30000143, "Stargate"), (30000143, 30001379, "Stargate"),
                (30001379, 30001376, "Stargate"), (30001376, 30002813, "Stargate"),
                (30002813, 30045346, "Stargate"), (30045346, 30045345, "Stargate"),
                (30045345, 30045353, "Stargate"), (30045353, 30045338, "Stargate"),
                (30045338, 30045344, "Stargate"), (30045344, 30003837, "Stargate"),
                (30003837, 30003836, "Stargate"), (30003836, 30003841, "Stargate"),
                (30003841, 30004046, "Stargate"), (30004046, 30004044, "Stargate"),
                (30004044, 30004043, "Stargate"), (30004043, 30004042, "Stargate"),
                (30004042, 30004040, "Stargate"), (30004040, 30004589, "Stargate"),
                (30004589, 30004586, "Stargate"), (30004586, 30004584, "Stargate"),
                (30004584, 30004554, "Stargate"), (30004554, 30004552, "Stargate"),
                (30004552, 30004553, "Stargate"), (30004553, 30004555, "Stargate"),
                (30004555, 30004557, "Stargate"), (30004557, 30004573, "Stargate"),
                (30004573, 30004574, "Stargate"), (30004574, 30004575, "Stargate"),
                (30004575, 30004578, "Stargate"), (30004578, 30004581, "Stargate"),
                (30004581, 30004583, "Stargate"), (30004583, 30004590, "Stargate"),
            ],
        )
        self.assertEqual(route_length(30000142, 30004590, mode="p_shortest", static_cache=True), 32)

    def test_jita_to_b170_safest_static_path_and_gates(self) -> None:
        self.assertEqual(
            route_path_nodes(30000142, 30004590, mode="p_safest", static_cache=True),
            [
                30000142, 30000144, 30000139, 30002802, 30002801, 30002800, 30002768, 30002765,
                30002764, 30002762, 30004974, 30004973, 30004975, 30003830, 30003833, 30003827,
                30003829, 30003836, 30003841, 30004046, 30004044, 30004043, 30004042, 30004040,
                30004589, 30004586, 30004584, 30004554, 30004552, 30004553, 30004555, 30004557,
                30004573, 30004574, 30004575, 30004578, 30004581, 30004583, 30004590,
            ],
        )
        self.assertEqual(
            route_path_gates(30000142, 30004590, mode="p_safest", static_cache=True),
            [
                (30000142, 30000144, "Stargate"), (30000144, 30000139, "Stargate"),
                (30000139, 30002802, "Stargate"), (30002802, 30002801, "Stargate"),
                (30002801, 30002800, "Stargate"), (30002800, 30002768, "Stargate"),
                (30002768, 30002765, "Stargate"), (30002765, 30002764, "Stargate"),
                (30002764, 30002762, "Stargate"), (30002762, 30004974, "Stargate"),
                (30004974, 30004973, "Stargate"), (30004973, 30004975, "Stargate"),
                (30004975, 30003830, "Stargate"), (30003830, 30003833, "Stargate"),
                (30003833, 30003827, "Stargate"), (30003827, 30003829, "Stargate"),
                (30003829, 30003836, "Stargate"), (30003836, 30003841, "Stargate"),
                (30003841, 30004046, "Stargate"), (30004046, 30004044, "Stargate"),
                (30004044, 30004043, "Stargate"), (30004043, 30004042, "Stargate"),
                (30004042, 30004040, "Stargate"), (30004040, 30004589, "Stargate"),
                (30004589, 30004586, "Stargate"), (30004586, 30004584, "Stargate"),
                (30004584, 30004554, "Stargate"), (30004554, 30004552, "Stargate"),
                (30004552, 30004553, "Stargate"), (30004553, 30004555, "Stargate"),
                (30004555, 30004557, "Stargate"), (30004557, 30004573, "Stargate"),
                (30004573, 30004574, "Stargate"), (30004574, 30004575, "Stargate"),
                (30004575, 30004578, "Stargate"), (30004578, 30004581, "Stargate"),
                (30004581, 30004583, "Stargate"), (30004583, 30004590, "Stargate"),
            ],
        )
        self.assertEqual(route_length(30000142, 30004590, mode="p_safest", static_cache=True), 38)

    def test_jita_to_b170_less_safe_static_path_and_gates(self) -> None:
        self.assertEqual(
            route_path_nodes(30000142, 30004590, mode="p_less_safe", static_cache=True),
            [
                30000142, 30000143, 30001379, 30001376, 30002813, 30045346, 30045345, 30045353,
                30045338, 30045344, 30003837, 30003836, 30003841, 30004046, 30004044, 30004043,
                30004042, 30004040, 30004589, 30004586, 30004584, 30004554, 30004552, 30004553,
                30004555, 30004557, 30004573, 30004574, 30004575, 30004578, 30004581, 30004583,
                30004590,
            ],
        )
        self.assertEqual(
            route_path_gates(30000142, 30004590, mode="p_less_safe", static_cache=True),
            [
                (30000142, 30000143, "Stargate"), (30000143, 30001379, "Stargate"),
                (30001379, 30001376, "Stargate"), (30001376, 30002813, "Stargate"),
                (30002813, 30045346, "Stargate"), (30045346, 30045345, "Stargate"),
                (30045345, 30045353, "Stargate"), (30045353, 30045338, "Stargate"),
                (30045338, 30045344, "Stargate"), (30045344, 30003837, "Stargate"),
                (30003837, 30003836, "Stargate"), (30003836, 30003841, "Stargate"),
                (30003841, 30004046, "Stargate"), (30004046, 30004044, "Stargate"),
                (30004044, 30004043, "Stargate"), (30004043, 30004042, "Stargate"),
                (30004042, 30004040, "Stargate"), (30004040, 30004589, "Stargate"),
                (30004589, 30004586, "Stargate"), (30004586, 30004584, "Stargate"),
                (30004584, 30004554, "Stargate"), (30004554, 30004552, "Stargate"),
                (30004552, 30004553, "Stargate"), (30004553, 30004555, "Stargate"),
                (30004555, 30004557, "Stargate"), (30004557, 30004573, "Stargate"),
                (30004573, 30004574, "Stargate"), (30004574, 30004575, "Stargate"),
                (30004575, 30004578, "Stargate"), (30004578, 30004581, "Stargate"),
                (30004581, 30004583, "Stargate"), (30004583, 30004590, "Stargate"),
            ],
        )
        self.assertEqual(route_length(30000142, 30004590, mode="p_less_safe", static_cache=True), 32)

    def test_jita_to_amarr_lengths_static_cache(self) -> None:
        self.assertEqual(route_length(30000142, 30002187, mode="p_shortest", static_cache=True), 11)
        self.assertEqual(route_length(30000142, 30002187, mode="p_safest", static_cache=True), 45)
        self.assertEqual(route_length(30000142, 30002187, mode="p_less_safe", static_cache=True), 40)

    def test_jita_systems_range_1_static_cache(self) -> None:
        self.assertEqual(
            sorted(systems_range(30000142, 1, static_cache=True)),
            [30000138, 30000140, 30000143, 30000144, 30000145, 30001363, 30002780],
        )


def _make_advanced_graph() -> DiGraph:
    graph = DiGraph()
    graph.add_node(1, security=0.9, type="solar_system")
    graph.add_node(2, security=0.5, type="solar_system")
    graph.add_node(3, security=0.1, type="solar_system")
    graph.add_node(4, security=0.1, type="solar_system")
    graph.add_edge(1, 2, p_shortest=1, p_safest=2, p_less_safe=1, type="Stargate")
    graph.add_edge(2, 3, p_shortest=1, p_safest=2, p_less_safe=1, type="Stargate")
    graph.add_edge(1, 3, p_shortest=5, p_safest=1, p_less_safe=6, type="Wormhole")
    return graph


class RoutePathAdvancedTest(SimpleTestCase):
    @patch("routing.routing.build")
    def test_route_path_nodes_same_source_destination(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        self.assertEqual(route_path_nodes(1, 1), [1])

    @patch("routing.routing.build")
    def test_route_path_nodes_raises_for_unknown_node(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        with self.assertRaises(NodeNotFound):
            route_path_nodes(999, 1)

    @patch("routing.routing.build")
    def test_route_path_nodes_raises_for_unreachable_destination(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        with self.assertRaises(NetworkXNoPath):
            route_path_nodes(1, 4)

    @patch("routing.routing.build")
    def test_route_path_uses_extra_edges(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        _graph, nodes = _route_path(
            1,
            4,
            edges=[(3, 4, {"p_shortest": 1, "p_safest": 1, "p_less_safe": 1, "type": "Bridge"})],
        )

        self.assertEqual(nodes, [1, 2, 3, 4])

    @patch("routing.routing.build")
    def test_route_path_forwards_static_cache_flag(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        route_path_nodes(1, 3, static_cache=True)

        mock_build.assert_called_once_with(True)

    @patch("routing.routing.build")
    def test_route_path_respects_weight_mode(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        shortest = route_path_nodes(1, 3, mode="p_shortest")
        safest = route_path_nodes(1, 3, mode="p_safest")

        self.assertEqual(shortest, [1, 2, 3])
        self.assertEqual(safest, [1, 3])


class SystemsRangeAdvancedTest(SimpleTestCase):
    @patch("routing.routing.build")
    def test_systems_range_zero_excludes_source(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        self.assertEqual(systems_range(1, 0), [])

    @patch("routing.routing.build")
    def test_systems_range_large_range_ignores_disconnected_nodes(self, mock_build: Mock) -> None:
        mock_build.return_value = _make_advanced_graph()

        self.assertEqual(sorted(systems_range(1, 99)), [2, 3])


class RouteCheckAdvancedTest(SimpleTestCase):
    @patch("routing.routing.route_path_nodes", return_value=[1, 2, 3])
    @patch("routing.routing.TrigInvasion.objects.filter")
    def test_route_check_triglavian_true_when_any_system_not_occupied(self, mock_filter: Mock, _mock_nodes: Mock) -> None:
        mock_filter.side_effect = [
            Mock(exists=Mock(return_value=True)),
            Mock(exists=Mock(return_value=False)),
        ]

        self.assertTrue(route_check_triglavian(1, 3))

    @patch("routing.routing.route_path_nodes", return_value=[1, 2, 3])
    @patch("routing.routing.TrigInvasion.objects.filter")
    def test_route_check_triglavian_false_when_all_systems_occupied(self, mock_filter: Mock, _mock_nodes: Mock) -> None:
        mock_filter.side_effect = [
            Mock(exists=Mock(return_value=True)),
            Mock(exists=Mock(return_value=True)),
            Mock(exists=Mock(return_value=True)),
        ]

        self.assertFalse(route_check_triglavian(1, 3))

    @patch("routing.routing.route_path_nodes", return_value=[1, 2, 3])
    @patch("routing.routing.TrigInvasion.objects.filter")
    def test_route_check_edencom_true_when_any_system_not_occupied(self, mock_filter: Mock, _mock_nodes: Mock) -> None:
        mock_filter.side_effect = [
            Mock(exists=Mock(return_value=True)),
            Mock(exists=Mock(return_value=False)),
        ]

        self.assertTrue(route_check_edencom(1, 3))

    @patch("routing.routing.route_path_nodes", return_value=[1, 2, 3])
    @patch("routing.routing.TrigInvasion.objects.filter")
    def test_route_check_edencom_false_when_all_systems_occupied(self, mock_filter: Mock, _mock_nodes: Mock) -> None:
        mock_filter.side_effect = [
            Mock(exists=Mock(return_value=True)),
            Mock(exists=Mock(return_value=True)),
            Mock(exists=Mock(return_value=True)),
        ]

        self.assertFalse(route_check_edencom(1, 3))
