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

# Visual Facts
1. The graph contains 5 nodes labeled A, B, C, D, and E
2. There are 7 edges in the graph:
   - A-B
   - A-C
   - A-D
   - B-D
   - B-E
   - C-D
3. All edges are undirected (shown by single lines)
4. All edges appear to have equal weight (shown by similar line styles)

# Visual Patterns
- All edges have equal weight (no different line styles or weights shown)
- The shortest path will be the one with the minimum number of edges


# 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', 'C', 'D'],
        'B': ['A', 'E', 'D'],
        'C': ['A', 'D'],
        'D': ['C', 'A', 'B'],
        'E': ['B']
    }

    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']),
        ('A', 'E', ['A', 'B', 'E']),
        ('B', 'D', ['B', 'D']),
        ('C', 'D', ['C', 'D']),
        ('B', 'E', ['B', 'E'])
    ]
    
    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}"
            )
            assert False, error_message
            
    multi_cases = [
        ('C', 'E', ['C', 'A', 'B', 'E'], ['C', 'D', 'B', 'A']),
        ('C', 'B', ['C', 'A', 'B'], ['C', 'D', 'B']),
    ]
    
    for start, end, expected_output_1, expected_output_2 in multi_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_1 and output != expected_output_2:
            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 1: {expected_output_1}\n"
                f"Expected Output 2: {expected_output_2}"
            )
            assert False, error_message

test()