# Problem Description
This is a graph pathfinding problem where we need to:
- Find the minimum cost path between two given nodes in an undirected weighted graph.
- Each node has an associated value.
- Each edge has a cost.
- The total path cost must follow a specific pattern based on the nodes' values and edge costs.
- We need to find the optimal (minimum cost) path from start to end node.

# Visual Facts
1. Graph Structure:
   - 6 nodes labeled A through F.
   - 5 edges connecting these nodes.
   - Graph is undirected (no arrows on edges).

2. Node Values:
   - A: 12
   - B: 3
   - C: -2
   - D: -8
   - E: -6
   - F: 4

3. Edge Costs:
   - A-B: 9
   - B-C: 5
   - B-D: 11
   - D-E: 2
   - D-F: 12

# Visual Patterns
1. **Cost Calculation Pattern**:
   - The cost of each edge appears to be the absolute value of the difference between two nodes
   - For example:
     - The cost of edge A-B is |12 - 3| = 9.
     - The cost of edge B-C is |3 - (-2)| = 5.
     - The cost of edge B-D is |3 - (-8)| = 11.
     - The cost of edge D-E is |-8 - (-6)| = 2.
     - The cost of edge D-F is |-8 - 4| = 12.

# Solution
import heapq

def solution(nodes: dict, edges: list, start: str, end: str) -> int:
    """
    Given the nodes and edges of a graph, determine the minimum path cost from a given starting node to an ending node.
    Please observe the example graph in the image to deduce the pattern calculating the path cost between two nodes.

    Input:
    - nodes: A dictionary where each key represents a node, and its associated value is the node's value.
            Example: {'A': 10, 'B': 20} indicates that node A has a value of 10, and node B has a value of 20.
    - edges: A list of tuples, each containing two nodes that are directly connected.
            Example: [('A', 'B'), ('B', 'C')] means node A is connected to node B, and node B is connected to node C.
    - start: The starting node where the path begins.
    - end: The ending node where the path terminates.

    Output:
    - Return the minimum cost required to travel from the start node to the end node. Return -1 if no path exists.
    """
    graph = {node: {} for node in nodes}
    for node1, node2 in edges:
        if node1 in graph and node2 in graph:
            graph[node1][node2] = abs(nodes[node1] - nodes[node2])
            graph[node2][node1] = abs(nodes[node1] - nodes[node2])
    pq = [(0, start)]
    visited = set()
    min_cost = {node: float('inf') for node in nodes}
    min_cost[start] = 0
    while pq:
        current_cost, current_node = heapq.heappop(pq)
        if current_node in visited:
            continue
        visited.add(current_node)
        if current_node == end:
            return current_cost
        for neighbor, weight in graph[current_node].items():
            if neighbor not in visited:
                new_cost = current_cost + weight
                if new_cost < min_cost[neighbor]:
                    min_cost[neighbor] = new_cost
                    heapq.heappush(pq, (new_cost, neighbor))
    return -1


# Test Script
def test():
    nodes = {'A': 10, 'B': 3, 'C': -2, 'D': -8, 'E': -6, 'F': 4}
    connections = [('A', 'B'), ('B', 'C'), ('B', 'D'), ('D', 'E'), ('D', 'F')]
    pairs = [('A', 'C'), ('B', 'E'), ('A', 'F'), ('A', 'D'), ('A', 'E'), ('B', 'F')]
    expected_results = [12, 13, 30, 18, 20, 23]
    
    for index, (start, end) in enumerate(pairs):
        try:
            output = solution(nodes, connections, start, end)
        except Exception as e:
            error_message = (
                f"An exception occurred while running the test case:\n"
                f"`nodes`: {nodes}\n"
                f"`edges`: {connections}\n"
                f"`start`: {start}\n"
                f"`end`: {end}\n\n"
                f"Exception: '{str(e)}'"
            )
            assert False, error_message
            
        if output != expected_results[index]:
            error_message = (
                f"The following Test Case failed:\n"
                f"`nodes`: {nodes}\n"
                f"`edges`: {connections}\n"
                f"`start`: {start}\n"
                f"`end`: {end}\n\n"
                f"Actual Output: {output}\n"
                f"Expected Output: {expected_results[index]}"
            )
            assert False, error_message

    nodes = {'A': 1, 'B': -2, 'C': 3, 'D': -4, 'E': 5, 'F': -6}
    connections = [('A', 'D'), ('D', 'E'), ('E', 'F'), ('E', 'B'), ('F', 'C'), ('D', 'B')]
    pairs = [('A', 'C'), ('B', 'E'), ('A', 'F'), ('C', 'D'), ('A', 'B')]
    expected_results = [34, 7, 25, 29, 7]
    for index, (start, end) in enumerate(pairs):
        try:
            output = solution(nodes, connections, start, end)
        except Exception as e:
            error_message = (
                f"An exception occurred while running the test case:\n"
                f"`nodes`: {nodes}\n"
                f"`edges`: {connections}\n"
                f"`start`: {start}\n"
                f"`end`: {end}\n\n"
                f"Exception: '{str(e)}'"
            )
            assert False, error_message
            
        if output != expected_results[index]:
            error_message = (
                f"The following Test Case failed:\n"
                f"`nodes`: {nodes}\n"
                f"`edges`: {connections}\n"
                f"`start`: {start}\n"
                f"`end`: {end}\n\n"
                f"Actual Output: {output}\n"
                f"Expected Output: {expected_results[index]}"
            )
            assert False, error_message

test()