# Problem Description
This is a shortest path finding problem in a directed graph. The task is to implement a function that finds the shortest path between two given nodes in a directed graph. The function should return the sequence of nodes that represents the shortest path from the start node to the end node.

# Visual Facts
1. The graph contains 5 nodes labeled A, B, C, D, and E
2. There are 6 directed edges in the graph:
   - A → B
   - C → A
   - D → C
   - B → D
   - E → B
   - E → D

3. Each edge is represented by a purple arrow
4. All edges appear to have the same visual weight/thickness
5. Each node is represented by a blue circle with a letter inside

# Visual Patterns
- Movement is only allowed in the direction of the arrows
- Since all edges are visually identical, they likely have equal weights
- This suggests we should count the number of edges (hops) to determine the shortest path


# Solution
def solution(start: str, end: str) -> list[str]:
    """
    Find the shortest path from one point to another

    Parameters:
        start (str): The starting node of the path.
        end (str): The ending node of the path.

    Returns:
        Optional[List[str]]: A list of nodes representing the path from start to end, if such a path exists. Returns None if no path exists.

    Example:
        path = solution('A', 'E') # Output might be ['A', 'B', 'E']
    """
    graph = {
        'A': ['B'],
        'B': ['D'],
        'C': ['A'],
        'D': ['C'],
        'E': ['B', 'D']
    }
    if start not in graph or end not in graph:
        return None

    queue = [[start]]  # Initialize the queue with the starting node in a path list
    visited = set()  # To keep track of visited nodes

    while queue:
        path = queue.pop(0)  # Get the first path from the queue (FIFO)
        node = path[-1]  # Get the last node in the path
        if node in visited:
            continue
        visited.add(node)
        # Check if we've reached the end node
        if node == end:
            return path
        # Explore adjacent nodes
        for neighbor in graph[node]:
            new_path = path + [neighbor]  # Create a new path including the neighbor
            queue.append(new_path)
    # If the loop ends, no path was found
    return None


# Test Script
def test():
    test_cases = [
        ('A', 'B', ['A', 'B']),
        ('E', 'C', ['E', 'D', 'C']),
        ('E', 'A', ['E', 'D', 'C', 'A']),
        ('B', 'D', ['B', 'D']),
        ('C', 'B', ['C', 'A', 'B']),
        ('C', 'D', ['C', 'A', 'B', 'D']),
        ('E', 'B', ['E', 'B'])
    ]
    
    for start, end, expected_output in test_cases:
        try:
            output = solution(start, end)
        except Exception as e:
            error_message = (
                f"An exception occurred while running the test case:\n"
                f"`start`: {start}\n"  
                f"`end`: {end}\n\n"
                f"Exception: '{str(e)}'"
            )
            assert False, error_message

        if output != expected_output:
            error_message = (
                f"The following Test Case failed:\n"
                f"`start`: {start}\n"
                f"`end`: {end}\n\n"
                f"Actual Output: {output}\n"
                f"Expected Output: {expected_output}\n\n"
                f"Graph Structure:\n"
                "{'A': ['B', 'C'], 'B': ['A', 'D'], 'C': ['A', 'D'], 'D': ['B', 'C', 'E'], 'E': ['B', 'D']}"
            )
            assert False, error_message

    none_cases = [
        ['C', 'E'],
        ['A', 'E']
    ]
    
    for start, end in none_cases:
        try:
            output = solution(start, end)
        except Exception as e:
            error_message = (
                f"An exception occurred while running the test case:\n"
                f"`start`: {start}\n"
                f"`end`: {end}\n\n"
                f"Exception: '{str(e)}'"
            )
            assert False, error_message
            
        if output is not None:
            error_message = (
                f"The following Test Case failed:\n"
                f"`start`: {start}\n"
                f"`end`: {end}\n\n"
                f"Actual Output: {output}\n"
                f"Expected Output: None\n\n"
                f"Graph Structure:\n"
                "{'A': ['B', 'C'], 'B': ['A', 'D'], 'C': ['A', 'D'], 'D': ['B', 'C', 'E'], 'E': ['B', 'D']}"
            )
            assert False, error_message

test()