"""
Behavioral tests for data structure patterns.

These tests verify the logic of data structure patterns like trees,
graphs, linked lists, and specialized collections without making actual
external calls.
"""

from dataclasses import dataclass
from typing import Any


class TestTreeStructures:
    """Tests for tree data structure patterns."""

    def test_binary_tree_traversal_inorder(self):
        """Test in-order tree traversal."""

        @dataclass
        class TreeNode:
            value: Any
            left: "TreeNode" = None
            right: "TreeNode" = None

        def inorder(node, result=None):
            if result is None:
                result = []
            if node:
                inorder(node.left, result)
                result.append(node.value)
                inorder(node.right, result)
            return result

        #       2
        #      / \
        #     1   3
        root = TreeNode(2, TreeNode(1), TreeNode(3))
        assert inorder(root) == [1, 2, 3]

    def test_binary_tree_traversal_preorder(self):
        """Test pre-order tree traversal."""

        @dataclass
        class TreeNode:
            value: Any
            left: "TreeNode" = None
            right: "TreeNode" = None

        def preorder(node, result=None):
            if result is None:
                result = []
            if node:
                result.append(node.value)
                preorder(node.left, result)
                preorder(node.right, result)
            return result

        root = TreeNode(2, TreeNode(1), TreeNode(3))
        assert preorder(root) == [2, 1, 3]

    def test_binary_tree_traversal_postorder(self):
        """Test post-order tree traversal."""

        @dataclass
        class TreeNode:
            value: Any
            left: "TreeNode" = None
            right: "TreeNode" = None

        def postorder(node, result=None):
            if result is None:
                result = []
            if node:
                postorder(node.left, result)
                postorder(node.right, result)
                result.append(node.value)
            return result

        root = TreeNode(2, TreeNode(1), TreeNode(3))
        assert postorder(root) == [1, 3, 2]

    def test_tree_height(self):
        """Test calculating tree height."""

        @dataclass
        class TreeNode:
            value: Any
            left: "TreeNode" = None
            right: "TreeNode" = None

        def height(node):
            if node is None:
                return 0
            return 1 + max(height(node.left), height(node.right))

        # Single node
        assert height(TreeNode(1)) == 1

        # Three levels
        root = TreeNode(1, TreeNode(2, TreeNode(3)), TreeNode(4))
        assert height(root) == 3

    def test_find_in_tree(self):
        """Test finding value in tree."""

        @dataclass
        class TreeNode:
            value: Any
            left: "TreeNode" = None
            right: "TreeNode" = None

        def find(node, value):
            if node is None:
                return False
            if node.value == value:
                return True
            return find(node.left, value) or find(node.right, value)

        root = TreeNode(1, TreeNode(2), TreeNode(3))
        assert find(root, 2) is True
        assert find(root, 5) is False


class TestTrieStructure:
    """Tests for Trie (prefix tree) patterns."""

    def test_trie_insert_and_search(self):
        """Test Trie insert and search."""

        class TrieNode:
            def __init__(self):
                self.children = {}
                self.is_end = False

        class Trie:
            def __init__(self):
                self.root = TrieNode()

            def insert(self, word):
                node = self.root
                for char in word:
                    if char not in node.children:
                        node.children[char] = TrieNode()
                    node = node.children[char]
                node.is_end = True

            def search(self, word):
                node = self.root
                for char in word:
                    if char not in node.children:
                        return False
                    node = node.children[char]
                return node.is_end

        trie = Trie()
        trie.insert("hello")
        trie.insert("help")
        assert trie.search("hello") is True
        assert trie.search("help") is True
        assert trie.search("hel") is False
        assert trie.search("world") is False

    def test_trie_prefix_search(self):
        """Test Trie prefix search."""

        class TrieNode:
            def __init__(self):
                self.children = {}
                self.is_end = False

        class Trie:
            def __init__(self):
                self.root = TrieNode()

            def insert(self, word):
                node = self.root
                for char in word:
                    if char not in node.children:
                        node.children[char] = TrieNode()
                    node = node.children[char]
                node.is_end = True

            def starts_with(self, prefix):
                node = self.root
                for char in prefix:
                    if char not in node.children:
                        return False
                    node = node.children[char]
                return True

        trie = Trie()
        trie.insert("hello")
        trie.insert("help")
        assert trie.starts_with("hel") is True
        assert trie.starts_with("wor") is False

    def test_trie_autocomplete(self):
        """Test Trie autocomplete suggestions."""

        class TrieNode:
            def __init__(self):
                self.children = {}
                self.is_end = False

        class Trie:
            def __init__(self):
                self.root = TrieNode()

            def insert(self, word):
                node = self.root
                for char in word:
                    if char not in node.children:
                        node.children[char] = TrieNode()
                    node = node.children[char]
                node.is_end = True

            def autocomplete(self, prefix):
                node = self.root
                for char in prefix:
                    if char not in node.children:
                        return []
                    node = node.children[char]
                return self._collect_words(node, prefix)

            def _collect_words(self, node, prefix):
                words = []
                if node.is_end:
                    words.append(prefix)
                for char, child in node.children.items():
                    words.extend(self._collect_words(child, prefix + char))
                return words

        trie = Trie()
        for word in ["hello", "help", "helper", "world"]:
            trie.insert(word)
        suggestions = trie.autocomplete("hel")
        assert "hello" in suggestions
        assert "help" in suggestions
        assert "helper" in suggestions
        assert "world" not in suggestions


class TestGraphStructures:
    """Tests for graph data structure patterns."""

    def test_adjacency_list_representation(self):
        """Test adjacency list graph representation."""

        def create_graph():
            return {}

        def add_edge(graph, u, v, directed=False):
            if u not in graph:
                graph[u] = []
            graph[u].append(v)
            if not directed:
                if v not in graph:
                    graph[v] = []
                graph[v].append(u)

        graph = create_graph()
        add_edge(graph, "A", "B")
        add_edge(graph, "A", "C")
        assert "B" in graph["A"]
        assert "C" in graph["A"]
        assert "A" in graph["B"]

    def test_bfs_traversal(self):
        """Test breadth-first search traversal."""

        def bfs(graph, start):
            visited = []
            queue = [start]
            while queue:
                node = queue.pop(0)
                if node not in visited:
                    visited.append(node)
                    queue.extend(
                        n for n in graph.get(node, []) if n not in visited
                    )
            return visited

        graph = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []}
        result = bfs(graph, "A")
        assert result[0] == "A"
        assert "B" in result
        assert "C" in result
        assert "D" in result

    def test_dfs_traversal(self):
        """Test depth-first search traversal."""

        def dfs(graph, start, visited=None):
            if visited is None:
                visited = []
            visited.append(start)
            for neighbor in graph.get(start, []):
                if neighbor not in visited:
                    dfs(graph, neighbor, visited)
            return visited

        graph = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []}
        result = dfs(graph, "A")
        assert result[0] == "A"
        assert len(result) == 4

    def test_detect_cycle(self):
        """Test detecting cycle in directed graph."""

        def has_cycle(graph):
            visited = set()
            rec_stack = set()

            def dfs(node):
                visited.add(node)
                rec_stack.add(node)
                for neighbor in graph.get(node, []):
                    if neighbor not in visited:
                        if dfs(neighbor):
                            return True
                    elif neighbor in rec_stack:
                        return True
                rec_stack.remove(node)
                return False

            for node in graph:
                if node not in visited:
                    if dfs(node):
                        return True
            return False

        # Acyclic graph
        acyclic = {"A": ["B"], "B": ["C"], "C": []}
        assert has_cycle(acyclic) is False

        # Cyclic graph
        cyclic = {"A": ["B"], "B": ["C"], "C": ["A"]}
        assert has_cycle(cyclic) is True

    def test_topological_sort(self):
        """Test topological sort of DAG."""

        def topological_sort(graph):
            visited = set()
            result = []

            def dfs(node):
                visited.add(node)
                for neighbor in graph.get(node, []):
                    if neighbor not in visited:
                        dfs(neighbor)
                result.insert(0, node)

            for node in graph:
                if node not in visited:
                    dfs(node)
            return result

        graph = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []}
        result = topological_sort(graph)
        assert result.index("A") < result.index("B")
        assert result.index("A") < result.index("C")
        assert result.index("B") < result.index("D")


class TestLinkedListStructures:
    """Tests for linked list patterns."""

    def test_singly_linked_list(self):
        """Test singly linked list operations."""

        @dataclass
        class Node:
            value: Any
            next: "Node" = None

        class LinkedList:
            def __init__(self):
                self.head = None

            def append(self, value):
                new_node = Node(value)
                if not self.head:
                    self.head = new_node
                    return
                current = self.head
                while current.next:
                    current = current.next
                current.next = new_node

            def to_list(self):
                result = []
                current = self.head
                while current:
                    result.append(current.value)
                    current = current.next
                return result

        ll = LinkedList()
        ll.append(1)
        ll.append(2)
        ll.append(3)
        assert ll.to_list() == [1, 2, 3]

    def test_linked_list_reverse(self):
        """Test reversing a linked list."""

        @dataclass
        class Node:
            value: Any
            next: "Node" = None

        def reverse(head):
            prev = None
            current = head
            while current:
                next_node = current.next
                current.next = prev
                prev = current
                current = next_node
            return prev

        def to_list(head):
            result = []
            while head:
                result.append(head.value)
                head = head.next
            return result

        # Create 1 -> 2 -> 3
        head = Node(1, Node(2, Node(3)))
        reversed_head = reverse(head)
        assert to_list(reversed_head) == [3, 2, 1]

    def test_detect_cycle_in_list(self):
        """Test detecting cycle in linked list."""

        @dataclass
        class Node:
            value: Any
            next: "Node" = None

        def has_cycle(head):
            if not head:
                return False
            slow = head
            fast = head
            while fast and fast.next:
                slow = slow.next
                fast = fast.next.next
                if slow == fast:
                    return True
            return False

        # No cycle
        no_cycle = Node(1, Node(2, Node(3)))
        assert has_cycle(no_cycle) is False

        # With cycle
        cycle_node = Node(2)
        with_cycle = Node(1, cycle_node)
        cycle_node.next = Node(3, cycle_node)  # 3 points back to 2
        assert has_cycle(with_cycle) is True

    def test_find_middle_node(self):
        """Test finding middle node of linked list."""

        @dataclass
        class Node:
            value: Any
            next: "Node" = None

        def find_middle(head):
            slow = head
            fast = head
            while fast and fast.next:
                slow = slow.next
                fast = fast.next.next
            return slow.value if slow else None

        # Odd length: 1 -> 2 -> 3 -> 4 -> 5
        head = Node(1, Node(2, Node(3, Node(4, Node(5)))))
        assert find_middle(head) == 3

        # Even length: 1 -> 2 -> 3 -> 4
        head = Node(1, Node(2, Node(3, Node(4))))
        assert find_middle(head) == 3


class TestStackPatterns:
    """Tests for stack data structure patterns."""

    def test_basic_stack_operations(self):
        """Test basic stack push/pop/peek."""

        class Stack:
            def __init__(self):
                self.items = []

            def push(self, item):
                self.items.append(item)

            def pop(self):
                if self.is_empty():
                    return None
                return self.items.pop()

            def peek(self):
                if self.is_empty():
                    return None
                return self.items[-1]

            def is_empty(self):
                return len(self.items) == 0

            def size(self):
                return len(self.items)

        stack = Stack()
        stack.push(1)
        stack.push(2)
        stack.push(3)
        assert stack.peek() == 3
        assert stack.pop() == 3
        assert stack.pop() == 2
        assert stack.size() == 1

    def test_balanced_parentheses(self):
        """Test checking balanced parentheses."""

        def is_balanced(s):
            stack = []
            pairs = {")": "(", "}": "{", "]": "["}
            for char in s:
                if char in "({[":
                    stack.append(char)
                elif char in ")}]":
                    if not stack or stack[-1] != pairs[char]:
                        return False
                    stack.pop()
            return len(stack) == 0

        assert is_balanced("()") is True
        assert is_balanced("({[]})") is True
        assert is_balanced("(]") is False
        assert is_balanced("((())") is False

    def test_min_stack(self):
        """Test stack that supports O(1) min operation."""

        class MinStack:
            def __init__(self):
                self.stack = []
                self.min_stack = []

            def push(self, val):
                self.stack.append(val)
                if not self.min_stack or val <= self.min_stack[-1]:
                    self.min_stack.append(val)

            def pop(self):
                val = self.stack.pop()
                if val == self.min_stack[-1]:
                    self.min_stack.pop()
                return val

            def get_min(self):
                return self.min_stack[-1] if self.min_stack else None

        ms = MinStack()
        ms.push(3)
        ms.push(1)
        ms.push(2)
        assert ms.get_min() == 1
        ms.pop()
        assert ms.get_min() == 1
        ms.pop()
        assert ms.get_min() == 3


class TestQueuePatterns:
    """Tests for queue data structure patterns."""

    def test_basic_queue_operations(self):
        """Test basic queue enqueue/dequeue."""

        class Queue:
            def __init__(self):
                self.items = []

            def enqueue(self, item):
                self.items.append(item)

            def dequeue(self):
                if self.is_empty():
                    return None
                return self.items.pop(0)

            def front(self):
                if self.is_empty():
                    return None
                return self.items[0]

            def is_empty(self):
                return len(self.items) == 0

        queue = Queue()
        queue.enqueue(1)
        queue.enqueue(2)
        queue.enqueue(3)
        assert queue.front() == 1
        assert queue.dequeue() == 1
        assert queue.dequeue() == 2

    def test_circular_queue(self):
        """Test circular queue with fixed size."""

        class CircularQueue:
            def __init__(self, capacity):
                self.capacity = capacity
                self.items = [None] * capacity
                self.front = 0
                self.rear = 0
                self.size = 0

            def enqueue(self, item):
                if self.is_full():
                    return False
                self.items[self.rear] = item
                self.rear = (self.rear + 1) % self.capacity
                self.size += 1
                return True

            def dequeue(self):
                if self.is_empty():
                    return None
                item = self.items[self.front]
                self.front = (self.front + 1) % self.capacity
                self.size -= 1
                return item

            def is_empty(self):
                return self.size == 0

            def is_full(self):
                return self.size == self.capacity

        cq = CircularQueue(3)
        assert cq.enqueue(1) is True
        assert cq.enqueue(2) is True
        assert cq.enqueue(3) is True
        assert cq.enqueue(4) is False  # Full
        assert cq.dequeue() == 1
        assert cq.enqueue(4) is True  # Space now

    def test_priority_queue(self):
        """Test priority queue using heap."""
        import heapq

        class PriorityQueue:
            def __init__(self):
                self.heap = []

            def push(self, priority, item):
                heapq.heappush(self.heap, (priority, item))

            def pop(self):
                if not self.heap:
                    return None
                return heapq.heappop(self.heap)[1]

            def peek(self):
                if not self.heap:
                    return None
                return self.heap[0][1]

        pq = PriorityQueue()
        pq.push(3, "low")
        pq.push(1, "high")
        pq.push(2, "medium")
        assert pq.pop() == "high"
        assert pq.pop() == "medium"
        assert pq.pop() == "low"


class TestHashMapPatterns:
    """Tests for hash map patterns."""

    def test_simple_hash_function(self):
        """Test simple hash function."""

        def simple_hash(key, size):
            h = 0
            for char in str(key):
                h = (h * 31 + ord(char)) % size
            return h

        # Same key should give same hash
        assert simple_hash("test", 100) == simple_hash("test", 100)
        # Different keys may give different hashes
        h1 = simple_hash("abc", 100)
        h2 = simple_hash("def", 100)
        # Just check they're valid indices
        assert 0 <= h1 < 100
        assert 0 <= h2 < 100

    def test_hash_map_with_chaining(self):
        """Test hash map with separate chaining."""

        class HashMap:
            def __init__(self, capacity=10):
                self.capacity = capacity
                self.buckets = [[] for _ in range(capacity)]

            def _hash(self, key):
                return hash(key) % self.capacity

            def put(self, key, value):
                bucket = self.buckets[self._hash(key)]
                for i, (k, v) in enumerate(bucket):
                    if k == key:
                        bucket[i] = (key, value)
                        return
                bucket.append((key, value))

            def get(self, key, default=None):
                bucket = self.buckets[self._hash(key)]
                for k, v in bucket:
                    if k == key:
                        return v
                return default

            def delete(self, key):
                bucket = self.buckets[self._hash(key)]
                for i, (k, v) in enumerate(bucket):
                    if k == key:
                        del bucket[i]
                        return True
                return False

        hm = HashMap()
        hm.put("a", 1)
        hm.put("b", 2)
        assert hm.get("a") == 1
        assert hm.get("b") == 2
        assert hm.get("c") is None
        hm.put("a", 10)  # Update
        assert hm.get("a") == 10
        hm.delete("a")
        assert hm.get("a") is None

    def test_lru_cache_using_dict(self):
        """Test LRU cache implementation."""
        from collections import OrderedDict

        class LRUCache:
            def __init__(self, capacity):
                self.capacity = capacity
                self.cache = OrderedDict()

            def get(self, key):
                if key not in self.cache:
                    return None
                self.cache.move_to_end(key)
                return self.cache[key]

            def put(self, key, value):
                if key in self.cache:
                    self.cache.move_to_end(key)
                self.cache[key] = value
                if len(self.cache) > self.capacity:
                    self.cache.popitem(last=False)

        lru = LRUCache(2)
        lru.put("a", 1)
        lru.put("b", 2)
        assert lru.get("a") == 1
        lru.put("c", 3)  # Evicts "b"
        assert lru.get("b") is None
        assert lru.get("c") == 3


class TestHeapPatterns:
    """Tests for heap data structure patterns."""

    def test_min_heap_operations(self):
        """Test min heap using heapq."""
        import heapq

        heap = []
        heapq.heappush(heap, 5)
        heapq.heappush(heap, 3)
        heapq.heappush(heap, 7)
        heapq.heappush(heap, 1)
        assert heapq.heappop(heap) == 1
        assert heapq.heappop(heap) == 3

    def test_max_heap_simulation(self):
        """Test max heap using negative values."""
        import heapq

        class MaxHeap:
            def __init__(self):
                self.heap = []

            def push(self, val):
                heapq.heappush(self.heap, -val)

            def pop(self):
                return -heapq.heappop(self.heap)

            def peek(self):
                return -self.heap[0] if self.heap else None

        mh = MaxHeap()
        mh.push(5)
        mh.push(3)
        mh.push(7)
        assert mh.pop() == 7
        assert mh.pop() == 5

    def test_k_smallest_elements(self):
        """Test finding k smallest elements."""
        import heapq

        def k_smallest(nums, k):
            return heapq.nsmallest(k, nums)

        assert k_smallest([5, 3, 8, 1, 9, 2], 3) == [1, 2, 3]

    def test_k_largest_elements(self):
        """Test finding k largest elements."""
        import heapq

        def k_largest(nums, k):
            return heapq.nlargest(k, nums)

        assert k_largest([5, 3, 8, 1, 9, 2], 3) == [9, 8, 5]

    def test_merge_k_sorted_lists(self):
        """Test merging k sorted lists."""
        import heapq

        def merge_k_sorted(lists):
            heap = []
            for i, lst in enumerate(lists):
                if lst:
                    heapq.heappush(heap, (lst[0], i, 0))
            result = []
            while heap:
                val, list_idx, elem_idx = heapq.heappop(heap)
                result.append(val)
                if elem_idx + 1 < len(lists[list_idx]):
                    heapq.heappush(
                        heap,
                        (lists[list_idx][elem_idx + 1], list_idx, elem_idx + 1),
                    )
            return result

        lists = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
        assert merge_k_sorted(lists) == [1, 2, 3, 4, 5, 6, 7, 8, 9]


class TestSetPatterns:
    """Tests for set data structure patterns."""

    def test_set_operations(self):
        """Test basic set operations."""
        set1 = {1, 2, 3, 4}
        set2 = {3, 4, 5, 6}

        assert set1.union(set2) == {1, 2, 3, 4, 5, 6}
        assert set1.intersection(set2) == {3, 4}
        assert set1.difference(set2) == {1, 2}
        assert set1.symmetric_difference(set2) == {1, 2, 5, 6}

    def test_subset_superset(self):
        """Test subset and superset checks."""
        parent = {1, 2, 3, 4, 5}
        child = {2, 3}

        assert child.issubset(parent) is True
        assert parent.issuperset(child) is True
        assert parent.issubset(child) is False

    def test_disjoint_sets(self):
        """Test disjoint set check."""
        set1 = {1, 2, 3}
        set2 = {4, 5, 6}
        set3 = {3, 4}

        assert set1.isdisjoint(set2) is True
        assert set1.isdisjoint(set3) is False

    def test_union_find(self):
        """Test Union-Find (Disjoint Set Union) structure."""

        class UnionFind:
            def __init__(self, n):
                self.parent = list(range(n))
                self.rank = [0] * n

            def find(self, x):
                if self.parent[x] != x:
                    self.parent[x] = self.find(self.parent[x])
                return self.parent[x]

            def union(self, x, y):
                px, py = self.find(x), self.find(y)
                if px == py:
                    return False
                if self.rank[px] < self.rank[py]:
                    px, py = py, px
                self.parent[py] = px
                if self.rank[px] == self.rank[py]:
                    self.rank[px] += 1
                return True

            def connected(self, x, y):
                return self.find(x) == self.find(y)

        uf = UnionFind(5)
        uf.union(0, 1)
        uf.union(2, 3)
        assert uf.connected(0, 1) is True
        assert uf.connected(2, 3) is True
        assert uf.connected(0, 2) is False
        uf.union(1, 2)
        assert uf.connected(0, 3) is True


class TestBitManipulation:
    """Tests for bit manipulation patterns."""

    def test_bit_operations(self):
        """Test basic bit operations."""

        # Check if bit is set
        def is_bit_set(num, pos):
            return (num >> pos) & 1 == 1

        # Set a bit
        def set_bit(num, pos):
            return num | (1 << pos)

        # Clear a bit
        def clear_bit(num, pos):
            return num & ~(1 << pos)

        # Toggle a bit
        def toggle_bit(num, pos):
            return num ^ (1 << pos)

        assert is_bit_set(5, 0) is True  # 5 = 101, bit 0 is set
        assert is_bit_set(5, 1) is False  # bit 1 is not set
        assert set_bit(4, 0) == 5  # 4 = 100, set bit 0 -> 101 = 5
        assert clear_bit(5, 0) == 4  # 5 = 101, clear bit 0 -> 100 = 4
        assert toggle_bit(5, 1) == 7  # 5 = 101, toggle bit 1 -> 111 = 7

    def test_count_set_bits(self):
        """Test counting set bits (population count)."""

        def count_bits(n):
            count = 0
            while n:
                count += n & 1
                n >>= 1
            return count

        assert count_bits(0) == 0
        assert count_bits(7) == 3  # 111
        assert count_bits(255) == 8  # 11111111

    def test_power_of_two(self):
        """Test checking if number is power of two."""

        def is_power_of_two(n):
            return n > 0 and (n & (n - 1)) == 0

        assert is_power_of_two(1) is True  # 2^0
        assert is_power_of_two(2) is True  # 2^1
        assert is_power_of_two(8) is True  # 2^3
        assert is_power_of_two(6) is False

    def test_single_number(self):
        """Test finding single number using XOR."""

        def find_single(nums):
            result = 0
            for num in nums:
                result ^= num
            return result

        # Every element appears twice except one
        assert find_single([2, 3, 2, 4, 3]) == 4
        assert find_single([1, 1, 2, 2, 3]) == 3

    def test_swap_without_temp(self):
        """Test swapping without temporary variable."""

        def swap(a, b):
            a = a ^ b
            b = a ^ b
            a = a ^ b
            return a, b

        assert swap(5, 10) == (10, 5)
        assert swap(0, 100) == (100, 0)
