import heapq
import matplotlib
import matplotlib.pyplot as plt
from math import radians, sin, cos, sqrt, atan2

############################################################
# EXCEPTIONS
############################################################

########################################
# Recognize bad user input as exceptions
########################################

######## Without Try-Except #######
# a = int(input("Tell me one number: "))
# b = int(input("Tell me another number: "))
# print("a + b =", a + b)
# print("a / b =", a / b)

######## Try-Except Basic Structure #######

# try:
#     a = int(input("Tell me one number: "))
#     b = int(input("Tell me another number: "))
#     print("a + b =", a + b)
#     print("a / b =", a / b)
# except:
#     print("Bug in user input.")

# print("Continue execution from here.")

######## Try-Except: Catching Multiple Exception Types #######
# try:
#     a = int(input("Tell me one number: "))
#     b = int(input("Tell me another number: "))
#     print("a + b =", a + b)
#     print("a / b =", a / b)
# except ValueError:
#     print("Could not convert to a number.")
# except ZeroDivisionError:
#     print("Can't divide by zero")
# except KeyboardInterrupt:
#     # Press Ctrl-C to generate a KeyboardInterrupt exception
#     print("User Interrupted Program")
# except:
#     print("Something went very wrong.")

# print("Continue execution from here.")


######## Try-Except: else and finally blocks #######
# try:
#     a = int(input("Tell me one number: "))
#     b = int(input("Tell me another number: "))
#     print("a + b =", a + b)
#     print("a / b =", a / b)
# except ValueError:
#     print("Could not convert to a number.")
# except ZeroDivisionError:
#     print("Can't divide by zero")
# except:
#     print("Something went very wrong.")
# else:
#     print("Everything Worked Out Fine!")
# finally:
#     print("This is executed independent of success or failure.")

# print("Continue execution from here.")

######## Try-Except: Accessing Error Object #######
# try:
#     a = int(input("Tell me one number: "))
#     b = int(input("Tell me another number: "))
#     print("a + b =", a + b)
#     print("a / b =", a / b)
# except ValueError as error:
#     print("Returned Error:", error)
#     print("Could not convert to a number.")
# except ZeroDivisionError as error:
#     print("Returned Error:", error)
#     print("Can't divide by zero")

# print("Continue execution from here.")

########################################
# Calculate average pacings for each jog
########################################

def collect_ratios_1(distances, times, speed_history):
    """
    Receive jogging distances in miles, times in minutes.
    Calculate and append mph average speeds to speed history list.
    """
    #convert minutes into times per hour
    times_hr = []
    for t in times:
        times_hr.append(t / 60)

    #compute miles per hour and append to speed history list
    for i in range(len(distances)):
        mph = distances[i] / times_hr[i]
        speed_history.append(mph)

# distances = [2, 5, 4]
# times = [16, 60, 40]
# speed_history = [6.5, 5.7]
# collect_ratios_1(distances, times, speed_history)
# print(f"{speed_history = }")

########################################

def collect_ratios_2(distances, times, speed_history):
    """
    Receive jogging distances in miles, times in minutes.
    Calculate and append mph average speeds to speed history list.
    """
    #convert minutes into times per hour
    times_hr = []
    for t in times:
        times_hr.append(t / 60)

    #compute miles per hour and append to speed history list
    for i in range(len(distances)):
        try:
            mph = distances[i] / times_hr[i]
            speed_history.append(mph)
        except ZeroDivisionError:
            print("Handling zero division")
            print("Appending nan")
            speed_history.append(float('nan')) # nan = not a number

# distances = [2, 5, 4]
# times = [16, 0, 40]
# speed_history = [6.5, 5.7]
# # collect_ratios_1(distances, times, speed_history)
# collect_ratios_2(distances, times, speed_history)
# print(f"{speed_history = }")

########################################

def collect_ratios_3(distances, times, speed_history):
    """
    Receive jogging distances in miles, times in minutes.
    Calculate and append mph average speeds to speed history list.
    """
    #convert minutes into times per hour
    times_hr = []
    for t in times:
        times_hr.append(t / 60)

    #compute miles per hour and append to speed history list
    for i in range(len(distances)):
        try:
            mph = distances[i] / times_hr[i]
            speed_history.append(mph)
        except ZeroDivisionError:
            print("Handling zero division")
            print("Appending nan")
            speed_history.append(float('nan'))
        except IndexError:
            print("Handling index out of range.")
            print("Skipping Data Entry.")

# distances = [2, 5, 4]
# times = [16, 0]
# speed_history = [6.5, 5.7]
# # collect_ratios_2(distances, times, speed_history)
# collect_ratios_3(distances, times, speed_history)
# print(f"{speed_history = }")

########################################

def collect_ratios_4(distances, times, speed_history):
    """
    Receive jogging distances in miles, times in minutes.
    Calculate and append mph average speeds to speed history list.
    """
    #convert minutes into times per hour
    times_hr = []
    for t in times:
        times_hr.append(t / 60)

    #compute miles per hour and append to speed history list
    for i in range(len(distances)):
        try:
            ratio = distances[i] / times_hr[i]
            speed_history.append(ratio)
        except ZeroDivisionError:
            print("Handling zero division")
            print("Appending nan")
            speed_history.append(float('nan'))
        except IndexError:
            print("Handling index out of range.")
            print("Skipping Data Entry.")

# distances = [2, 5, 4]
# times = [16, 7, 10]
# speed_history = (6.5, 5.7)
# collect_ratios_4(distances, times, speed_history)

# ###Catching Error in Global Frame
# try:
#     collect_ratios_4(distances, times, speed_history)
# except AttributeError as error:
#     print("Returned Error:", error)
# print(f"{speed_history = }")


########################################

def collect_ratios_5(distances, times, speed_history):
    """
    Receive jogging distances in miles, times in minutes.
    Calculate and append mph average speeds to speed history list.
    """
    #convert minutes into times per hour
    times_hr = []
    for t in times:
        times_hr.append(t / 60)

    #compute miles per hour and append to speed history list
    for i in range(len(distances)):
        ratio = distances[i] / times_hr[i]
        if ratio > 30: raise ValueError(f"Jogging Speed is unrealistically high with {ratio} miles per hour.")
        speed_history.append(ratio)

# distances = [10, 5, 4]
# times = [16, 0]
# speed_history = [6.5, 5.7]
# try:
#     collect_ratios_5(distances, times, speed_history)
# except ValueError as error:
#     print("Returned Error:", error)
# print(f"{speed_history = }")

########################################
# Calculate average grades
########################################
######## Version 1: No Assertions #######
def compute_average_grades1(records):
    """
    records is a list, each element of which is a a student record comprising
        a list of two elements:
        + first, a list of [first name, last name]
        + second, a list of grades as numbers

    Return a new list that mirrors class_list, except each student record
        now has a third element that is the average of their grades
    """
    new_stats = []
    for student_record in records:
        name = student_record[0]
        grades = student_record[1]
        average = avg1(grades, name)
        new_stats.append([name, grades, average])
    return new_stats

def avg1(grades, name):
    return sum(grades) / len(grades)

# grades = [[['peter', 'parker'], [10.0, 5.0, 8.5]],
#           [['bruce', 'wayne'], [10.0, 8.0, 7.4]]]

# averaged_grades = compute_average_grades1(grades)
# for record in averaged_grades:
#     print(record)

######## Version 2: With Assertion #######
def compute_average_grades2(records):
    """
    records is a list, each element of which is a a student record comprising
        a list of two elements:
        + first, a list of [first name, last name]
        + second, a list of grades as numbers

    Return a new list that mirrors class_list, except each student record
        now has a third element that is the average of their grades
    """
    new_stats = []
    for record in records:
        name = record[0]
        grades = record[1]
        average = avg2(grades, name)
        new_stats.append([name, grades, average])
    return new_stats

def avg2(grades, name):
    assert len(grades) != 0, f"no grades data for {name}"
    return sum(grades) / len(grades)

grades = [[['peter', 'parker'], [10.0, 5.0, 8.5]],
          [['bruce', 'wayne'], [10.0, 8.0, 7.4]],
          [['thor', 'odinson'], []],
          [['steve', 'rogers'], [10.0, 8.0, 9.6]]]

# # averaged_grades = compute_average_grades1(grades)
# averaged_grades = compute_average_grades2(grades)
# for record in averaged_grades:
#     print(record)


########################################
# Representing graphs
########################################

class Node:
    """Represents a generic graph node with only a name (string)."""

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return self.name == str(other)

    def __hash__(self):
        # Nodes are uniquely identified by their name
        return hash(self.name)


class MapNode(Node):
    """Extends Node with (x, y) coordinate information."""

    def __init__(self, name, coords):
        super().__init__(name)
        if not (isinstance(coords, tuple) and len(coords) == 2):
            raise ValueError("coords must be a tuple (x, y)")
        self.coords = coords  # e.g., (-71.0589, 42.3601)


class SimpleDigraph:
    """Represents a weighted directed graph using Nodes as keys."""

    def __init__(self, nodes=()):
        self._edges = {}  # dict: Node -> dict(Node -> weight)
        for node in nodes:
            self.add_node(node)

    def add_node(self, node):
        self._edges[node] = {}

    def get_node(self, id):
        if id in self._edges:
            return id

    def add_edge(self, src, dest, weight=1):
        """Add a directed edge between two Node objects (or names)."""
        src = self.get_node(src)
        dest = self.get_node(dest)
        self._edges[src][dest] = weight

    def get_all_nodes(self):
        return list(self._edges.keys())

    def outgoing_edges_of(self, node):
        return self._edges[node].copy()

    def children_of(self, node):
        return list(self._edges[node].keys())


class Digraph:
    """Represents a weighted directed graph using Nodes as keys."""

    def __init__(self, nodes=()):
        self._edges = {}  # dict: Node -> dict(Node -> weight)
        for node in nodes:
            self.add_node(node)

    def add_node(self, node):
        """Add a Node object to the graph."""
        if not isinstance(node, Node):
            raise TypeError("add_node expects a Node instance")
        if node in self._edges:
            raise ValueError(f"Duplicate node: {node.name}")
        self._edges[node] = {}

    def get_node(self, id):
        """Internal helper: resolve a node name or return node directly."""
        if isinstance(id, str) or isinstance(id, Node):
            for n in self._edges:
                if n.name == id:
                    return n
            raise ValueError(f"Unknown node name: '{id}'")
        raise TypeError(f"Expected Node or str, got {type(id).__name__}")

    def add_edge(self, src, dest, weight=1):
        """Add a directed edge between two Node objects (or names)."""
        src = self.get_node(src)
        dest = self.get_node(dest)
        self._edges[src][dest] = weight

    def get_all_nodes(self):
        return list(self._edges.keys())

    def outgoing_edges_of(self, node):
        return self._edges[node].copy()

    def children_of(self, node):
        return list(self._edges[node].keys())

    def __str__(self):
        vals = []
        for src in sorted(self._edges.keys(), key=lambda n: n.name):
            entry = f"{src}: "
            if self._edges[src]:
                entry += ", ".join(f"{dest.name}({w})" for dest, w in self._edges[src].items())
            vals.append(entry)
        return "\n".join(vals)


class Graph(Digraph):
    def add_edge(self, node1, node2, weight=1):
        super().add_edge(node1, node2, weight)
        super().add_edge(node2, node1, weight)


class MapGraph(Graph):
    """Graph that stores MapNode instances and offers geographic utilities."""

    EARTH_RADIUS_M = 6_371_000  # mean Earth radius in meters

    def __init__(self, nodes=(), max_speed_kmh=100):
        super().__init__(nodes)
        self.max_speed_kmh = max_speed_kmh

    def add_node(self, node):
        if not isinstance(node, MapNode):
            raise TypeError("MapGraph expects MapNode instances")
        super().add_node(node)

    def _resolve_mapnode(self, node):
        resolved = self.get_node(node)
        if not isinstance(resolved, MapNode):
            raise TypeError("MapGraph operations require MapNode instances but got: " + str(type(resolved)))
        return resolved

    def _coords(self, node):
        return self._resolve_mapnode(node).coords

    def haversine_distance(self, node1, node2):
        """Return the great-circle distance between two nodes in meters."""
        lon1, lat1 = self._coords(node1)
        lon2, lat2 = self._coords(node2)
        phi1, phi2 = radians(lat1), radians(lat2)
        dphi = radians(lat2 - lat1)
        dlambda = radians(lon2 - lon1)
        a = sin(dphi / 2)**2 + cos(phi1) * cos(phi2) * sin(dlambda / 2)**2
        c = 2 * atan2(sqrt(a), sqrt(1 - a))
        return MapGraph.EARTH_RADIUS_M * c

    def distance(self, node1, node2):
        """Return the Euclidean distance between two nodes in coordinate space."""
        x1, y1 = self._coords(node1)
        x2, y2 = self._coords(node2)
        return sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)

    def dist_great_circle(self, node1, node2):
        """Return the great-circle distance between nodes in kilometers."""
        return self.haversine_distance(node1, node2) / 1000

    def add_edge(self, node1, node2, weight=None):
        """
        Add an undirected edge whose weight equals the great-circle distance
        between the two endpoints (in kilometers).
        """
        node1_resolved = self._resolve_mapnode(node1)
        node2_resolved = self._resolve_mapnode(node2)
        actual_weight = self.distance(node1_resolved, node2_resolved)
        super().add_edge(node1_resolved, node2_resolved, actual_weight)


########################################
# Example graphs
########################################


def build_city_nodes_graph():
    city_coords = {
        "Boston": (-71.0589, 42.3601),
        "Providence": (-71.4128, 41.8240),
        "New York": (-74.0060, 40.7128),
        "Chicago": (-87.6298, 41.8781),
        "Denver": (-104.9903, 39.7392),
        "Pittsburgh": (-79.9959, 40.4406),
        "Salt Lake City": (-111.8910, 40.7608),
        "San Francisco": (-122.4194, 37.7749),
        "Houston": (-95.3698, 29.7604),
        "Bozeman": (-111.0429, 45.6770),
        "Seattle": (-122.3321, 47.6062),
        "Minneapolis": (-93.2650, 44.9778),
        "Los Angeles": (-118.2437, 34.0522),
        "Atlanta": (-84.3880, 33.7490),
        "Miami": (-80.1918, 25.7617),
        "Philadelphia": (-75.1652, 39.9526),
        "Phoenix": (-112.0740, 33.4484),
        "San Diego": (-117.1611, 32.7157),
        "Dallas": (-96.7970, 32.7767),
        "Portland": (-122.6587, 45.5122),
        "Las Vegas": (-115.1398, 36.1699),
        "Austin": (-97.7431, 30.2672),
        "Nashville": (-86.7816, 36.1627),
        "Indianapolis": (-86.1581, 39.7684),
        "Charlotte": (-80.8431, 35.2271),
        "Cleveland": (-81.6944, 41.4993)
    }

    # Create MapNode objects for each city
    nodes = {name: MapNode(name, coords) for name, coords in city_coords.items()}

    # Create the graph with these nodes (no edges yet)
    g = MapGraph(nodes.values())

    g.add_edge('Boston', 'Cleveland')
    g.add_edge('Boston', 'Providence')
    g.add_edge('Boston', 'New York')

    g.add_edge('Cleveland', 'Minneapolis')
    g.add_edge('Cleveland', 'Chicago')
    g.add_edge('Cleveland', 'Pittsburgh')

    g.add_edge('Providence', 'Boston')
    g.add_edge('Providence', 'New York')

    g.add_edge('New York', 'Cleveland')
    g.add_edge('New York', 'Pittsburgh')
    g.add_edge('New York', 'Philadelphia')

    g.add_edge('Philadelphia', 'Indianapolis')
    g.add_edge('Philadelphia', 'Charlotte')

    g.add_edge('Pittsburgh', 'Indianapolis')

    g.add_edge('Chicago', 'Minneapolis')
    g.add_edge('Chicago', 'Denver')
    g.add_edge('Chicago', 'Indianapolis')

    g.add_edge('Charlotte', 'Indianapolis')
    g.add_edge('Charlotte', 'Nashville')
    g.add_edge('Charlotte', 'Atlanta')

    g.add_edge('Indianapolis', 'Denver')
    g.add_edge('Indianapolis', 'Nashville')

    g.add_edge('Minneapolis', 'Bozeman')

    g.add_edge('Atlanta', 'Dallas')
    g.add_edge('Atlanta', 'Miami')
    g.add_edge('Atlanta', 'Houston')

    g.add_edge('Miami', 'Houston')

    g.add_edge('Dallas', 'Denver')
    g.add_edge('Dallas', 'Las Vegas')
    g.add_edge('Dallas', 'Phoenix')
    g.add_edge('Dallas', 'San Diego')
    g.add_edge('Dallas', 'Austin')
    g.add_edge('Dallas', 'Houston')

    g.add_edge('Houston', 'Austin')

    g.add_edge('Austin', 'San Diego')

    g.add_edge('Bozeman', 'Seattle')
    g.add_edge('Bozeman', 'Salt Lake City')

    g.add_edge('Nashville', 'Denver')
    g.add_edge('Nashville', 'Dallas')

    g.add_edge('Denver', 'Bozeman')
    g.add_edge('Denver', 'Salt Lake City')
    g.add_edge('Denver', 'Las Vegas')

    g.add_edge('Salt Lake City', 'Seattle')
    g.add_edge('Salt Lake City', 'Portland')
    g.add_edge('Salt Lake City', 'San Francisco')

    g.add_edge('Las Vegas', 'San Francisco')
    g.add_edge('Las Vegas', 'Los Angeles')
    g.add_edge('Las Vegas', 'Phoenix')

    g.add_edge('Phoenix', 'San Diego')

    g.add_edge('Seattle', 'Portland')

    g.add_edge('Portland', 'San Francisco')

    g.add_edge('San Francisco', 'Los Angeles')

    g.add_edge('Los Angeles', 'San Diego')

    return g



def visualize_graph(graph, display_names=False, color = "C0"):
    Lx, Ly, Lc = [], [], []
    def draw_line(n1, n2):
        Lx.append(n1.coords[0])
        Lx.append(n2.coords[0])
        Ly.append(n1.coords[1])
        Ly.append(n2.coords[1])
        Lx.append(None)
        Ly.append(None)

    for n1 in graph.get_all_nodes():
        if display_names:
            plt.text(n1.coords[0], n1.coords[1], n1.name)
        for n2 in graph.outgoing_edges_of(n1):
            w = g.outgoing_edges_of(n1)[n2]
            draw_line(n1, n2)

    return plt.plot(Lx, Ly, linewidth=1, color=color)

def visualize_path(path, line_width = 10, color = "C1", linestyle='-'):
    Lx, Ly, Lc = [], [], []
    #path is a list of nodes
    prev_node = None
    for node in path:
        if prev_node:
            Lx.append(prev_node.coords[0])
            Lx.append(node.coords[0])
            Ly.append(prev_node.coords[1])
            Ly.append(node.coords[1])
        prev_node = node
    plt.plot(Lx, Ly, linewidth=line_width,  color = color, linestyle=linestyle)

def dijkstra_heap(graph, start, goal, visualize = False, pause=0.5):
    start_node = graph.get_node(start)
    goal_node = graph.get_node(goal)

    # Initialize heap (priority queue) with (cost, path)
    queue = [(0, [start_node])]
    heapq.heapify(queue)  # ensures it's a valid heap
    visited = set()

    while queue:
        # Pop the smallest-cost path
        cost, path = heapq.heappop(queue)
        current_node = path[-1]

        # Skip if already processed
        if current_node in visited:
            continue
        visited.add(current_node)
        print(f"✅ Finished {current_node!s} with cost {cost}. Finished set: {{"
              f"{', '.join(str(node) for node in visited)}}}")

        if visualize:
            plt.clf()
            visualize_graph(graph, True)
            visualize_path(path)
            plt.title("Dijkstra")
            plt.plot(start_node.coords[0], start_node.coords[1], 'o', color='red',markersize=10, )
            plt.plot(goal_node.coords[0], goal_node.coords[1], 'o', color='red',markersize=10, )
            plt.draw()
            plt.pause(pause)

        # Stop when we reach the goal
        if current_node == goal_node:
            return cost, path

        # Expand neighbors
        for neighbor, weight in graph.outgoing_edges_of(current_node).items():
            if neighbor in visited:
                continue
            new_cost = cost + weight
            new_path = path + [neighbor]
            heapq.heappush(queue, (new_cost, new_path))
            # print(f"  ➕ Added path {current_node!s} → {neighbor!s} (total cost {new_cost})")

        # Optional: pretty-print queue contents for debugging
        pretty_queue = [(c, [str(n) for n in p]) for c, p in queue]
        # print(f"Current queue: {pretty_queue}\n")

    # If no path found
    return None

continue_processing = False

def pause_until_key_pressed(actually_pause_now = True):
    def on_key(event):
        if event.key =='q':
            sys.exit()
        global continue_processing
        continue_processing = not continue_processing
        if not continue_processing:
            while not continue_processing:
                plt.pause(0.1)  # Yield control to the event loop


    global continue_processing
    continue_processing = not actually_pause_now
    fig = plt.gcf()
    fig.canvas.mpl_connect('key_press_event', on_key)
    while not continue_processing:
        plt.pause(0.1)  # Yield control to the event loop



def astar_heap(graph, start, goal, visualize=False, pause=0.5):
    start_node = graph.get_node(start)
    goal_node = graph.get_node(goal)

    # Initialize heap with (f_score, g_score, path)
    # f_score = g_score + h_score (where h_score is the heuristic)
    g_score = 0  # Cost from start to current node
    h_score = graph.distance(start_node, goal_node)  # Heuristic: Euclidean distance to goal
    f_score = g_score + h_score
    queue = [(f_score, g_score, [start_node])]
    heapq.heapify(queue)
    visited = set()

    while queue:
        # Pop the path with smallest f_score

        f_score, g_score, path = heapq.heappop(queue)
        current_node = path[-1]
        print(f"A*: Exploring {current_node!s} with f_score {f_score} + g_score {g_score} = {g_score + h_score}")

        # Skip if already processed
        if current_node in visited:
            continue
        visited.add(current_node)
        # print(f"✅ Finished {current_node!s} with f_score {f_score}. Finished set: {{"
        #       f"{', '.join(str(node) for node in visited)}}}")

        if visualize:
            plt.clf()
            visualize_graph(graph, True)
            visualize_path(path)
            plt.title("A*")
            if current_node == goal_node:
                pause_until_key_pressed()
            plt.plot(start_node.coords[0], start_node.coords[1], 'o', color='red',markersize=10)
            plt.plot(goal_node.coords[0], goal_node.coords[1], 'o', color='red',markersize=10)
            visualize_path([goal_node, current_node], 4, "red", linestyle=':')
            plt.draw()
            plt.pause(pause)

        # Stop when we reach the goal
        if current_node == goal_node:
            return g_score, path

        # Expand neighbors
        for neighbor, edge_weight in graph.outgoing_edges_of(current_node).items():
            if neighbor in visited:
                continue
            new_g_score = g_score + edge_weight
            new_h_score = graph.distance(neighbor, goal_node)  # Heuristic estimate
            new_f_score = new_g_score + new_h_score
            new_path = path + [neighbor]
            heapq.heappush(queue, (new_f_score, new_g_score, new_path))
            # print(f"  ➕ Added path {current_node!s} → {neighbor!s} (f_score={new_f_score:.2f}, g={new_g_score:.2f}, h={new_h_score:.2f})")

        # Optional: pretty-print queue contents for debugging
        pretty_queue = [(f, g, [str(n) for n in p]) for f, g, p in queue]
        # print(f"Current queue: {pretty_queue}\n")

    # If no path found
    return None

########################################
# Test Dijkstra and A* on city graph
########################################



def test_dijkstra():
    plt.ion()
    plt.figure(1)
    visualize_graph(g, True)
    plt.plot(start_node.coords[0], start_node.coords[1], 'o', color='red',markersize=10)
    plt.plot(end_node.coords[0], end_node.coords[1], 'o', color='red',markersize=10)
    plt.title("Dijkstra")
    plt.show(block=False)
    plt.draw()
    pause_until_key_pressed()
    result_dijkstra = dijkstra_heap(g, start, end, visualize=True)

def test_astar():
    plt.ion()
    plt.clf()
    visualize_graph(g, True)
    plt.plot(start_node.coords[0], start_node.coords[1], 'o', color='red',markersize=10)
    plt.plot(end_node.coords[0], end_node.coords[1], 'o', color='red',markersize=10)
    plt.title("A*")
    plt.show(block=False)
    plt.draw()
    pause_until_key_pressed()

    result_astar = astar_heap(g, start, end, visualize=True)

g = build_city_nodes_graph()
start = 'Boston'
end = 'Phoenix'
start_node = g.get_node(start)
end_node = g.get_node(end)

# test_dijkstra()
# test_astar()
