import json

import pytest

import stuff
import stdlib


@pytest.mark.parametrize(
    ("n", "expected"),
    [
        (0, 0),
        (1, 1),
        (5, 2),
        (4, 2),
        (8, 2),
        (9, 3),
    ],
)
def test_find_sqrt_ish(n: int, expected: int):
    assert stuff.find_sqrt_ish(n) == expected


@pytest.mark.parametrize(
    ("ops", "expected"),
    [
        (
            (
                [
                    ("new",),
                    ("push", -2),
                    ("push", 0),
                    ("push", -3),
                    ("getMin",),
                    ("pop",),
                    ("top",),
                    ("getMin",),
                ]
            ),
            [
                None,
                None,
                None,
                None,
                -3,
                None,
                0,
                -2,
            ],
        )
    ],
)
def test_min_stack(ops: list[tuple[str] | tuple[str, int]], expected: list[int | None]):
    returned: list[int | None] = []
    inst: stuff.MinStack | None = None

    for op in ops:
        if len(op) == 1:
            if op[0] == "new":
                inst = stuff.MinStack()
                returned.append(None)
                continue

            returned.append(getattr(inst, op[0])())
            continue

        method, arg = op
        returned.append(getattr(inst, method)(arg))

    assert returned == expected


@pytest.mark.parametrize(
    ("head", "expected"),
    [
        (None, None),
        (
            stdlib.ListNode(
                4, stdlib.ListNode(2, stdlib.ListNode(1, stdlib.ListNode(3)))
            ),
            stdlib.ListNode(
                1, stdlib.ListNode(2, stdlib.ListNode(3, stdlib.ListNode(4)))
            ),
        ),
        (
            stdlib.ListNode(
                4,
                stdlib.ListNode(
                    19,
                    stdlib.ListNode(
                        14,
                        stdlib.ListNode(
                            5,
                            stdlib.ListNode(
                                -3,
                                stdlib.ListNode(
                                    1,
                                    stdlib.ListNode(
                                        8,
                                        stdlib.ListNode(
                                            5, stdlib.ListNode(11, stdlib.ListNode(15))
                                        ),
                                    ),
                                ),
                            ),
                        ),
                    ),
                ),
            ),
            stdlib.ListNode(
                -3,
                stdlib.ListNode(
                    1,
                    stdlib.ListNode(
                        4,
                        stdlib.ListNode(
                            5,
                            stdlib.ListNode(
                                5,
                                stdlib.ListNode(
                                    8,
                                    stdlib.ListNode(
                                        11,
                                        stdlib.ListNode(
                                            14, stdlib.ListNode(15, stdlib.ListNode(19))
                                        ),
                                    ),
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ],
)
def test_sort_linked_list(
    head: stdlib.LinkedListNode | None, expected: stdlib.LinkedListNode | None
):
    if head is None:
        assert stuff.sort_linked_list(head) == expected
        return

    assert stuff.linked_list_to_list(
        stuff.sort_linked_list(head)
    ) == stuff.linked_list_to_list(expected)


@pytest.mark.parametrize(
    ("root", "expected"),
    [
        (
            stdlib.Node(
                1,
                left=stdlib.Node(2, left=stdlib.Node(4), right=stdlib.Node(5)),
                right=stdlib.Node(3, right=stdlib.Node(7)),
            ),
            [1, None, 2, 3, None, 4, 5, 7, None],
        ),
    ],
)
def test_connect_binary_tree_right(
    root: stdlib.ConnectableBinaryTreeNode | None, expected: list[int | None] | None
):
    if expected is None:
        assert root is None
        return

    connected, serialized = stuff.connect_binary_tree_right(root)
    assert connected is not None
    assert serialized == expected


@pytest.mark.parametrize(
    ("root", "expected"),
    [
        (
            stdlib.Node(
                4,
                right=stdlib.Node(0),
                left=stdlib.Node(9, right=stdlib.Node(1), left=stdlib.Node(5)),
            ),
            1026,
        ),
    ],
)
def test_connect_binary_tree_sum_numbers(
    root: stdlib.BinaryTreeNode | None, expected: int
):
    assert stuff.sum_binary_tree_path_ints(root) == expected


@pytest.mark.parametrize(
    ("inlist", "expected"),
    [
        (
            [3, 5, 1, 6, 2, 0, 8, None, None, 7, 4],
            stdlib.TreeNode(
                3,
                left=stdlib.TreeNode(
                    5,
                    left=stdlib.TreeNode(6),
                    right=stdlib.TreeNode(
                        2,
                        left=stdlib.TreeNode(7),
                        right=stdlib.TreeNode(4),
                    ),
                ),
                right=stdlib.TreeNode(
                    1,
                    left=stdlib.TreeNode(0),
                    right=stdlib.TreeNode(8),
                ),
            ),
        ),
    ],
)
def test_binary_tree_from_list(
    inlist: list[int | None], expected: stdlib.BinaryTreeNode | None
):
    assert stuff.binary_tree_from_list(inlist) == expected


@pytest.mark.parametrize(
    ("preorder", "inorder", "expected"),
    [
        (
            [3, 9, 20, 15, 7],
            [9, 3, 15, 20, 7],
            stdlib.TreeNode(
                3,
                left=stdlib.TreeNode(9),
                right=stdlib.TreeNode(
                    20, left=stdlib.TreeNode(15), right=stdlib.TreeNode(7)
                ),
            ),
        ),
        (
            [-1],
            [-1],
            stdlib.TreeNode(-1),
        ),
        (
            [1, 2],
            [1, 2],
            stdlib.TreeNode(1, right=stdlib.TreeNode(2)),
        ),
    ],
)
def test_binary_tree_from_preorder_inorder(
    preorder: list[int], inorder: list[int], expected: stdlib.BinaryTreeNode
):
    assert stuff.binary_tree_from_preorder_inorder(preorder, inorder) == expected


@pytest.mark.parametrize(
    ("board", "expected"),
    [
        (
            [2, 3, 1, 1, 4],
            [
                [0, 1, 2, 3, 4],
                [0, 1, 3, 4],
                [0, 1, 4],
                [0, 2, 3, 4],
            ],
        ),
    ],
)
def test_collect_complete_jump_paths_from_board(
    board: list[int], expected: list[list[int]]
):
    assert list(sorted(stuff.collect_complete_jump_paths_from_board(board))) == expected


@pytest.mark.parametrize(
    ("board", "expected"),
    [
        (
            [2, 3, 1, 1, 4],
            2,
        ),
        (
            [2, 3, 0, 1, 4],
            2,
        ),
        (
            [1],
            0,
        ),
        (
            [1, 2],
            1,
        ),
        (
            [6, 2, 6, 1, 7, 9, 3, 5, 3, 7, 2, 8, 9, 4, 7, 7, 2, 2, 8, 4, 6, 6, 1, 3],
            4,
        ),
        (
            [3, 4, 3, 2, 5, 4, 3],
            3,
        ),
        (
            [3, 2, 1],
            1,
        ),
        (
            [1, 2, 3],
            2,
        ),
    ],
)
def test_count_min_jumps_from_board(board: list[int], expected: int):
    assert stuff.count_min_jumps_from_board(board) == expected


@pytest.mark.parametrize(
    ("citations", "expected"),
    [
        (
            [3, 0, 6, 1, 5],
            3,
        ),
        (
            [1, 3, 1],
            1,
        ),
        (
            [100],
            1,
        ),
    ],
)
def test_h_index(citations: list[int], expected: int):
    assert stuff.h_index(citations) == expected


@pytest.mark.parametrize(
    ("cls",),
    [
        (stuff.SlowRandomizedSet,),
        (stuff.RandomizedSet,),
    ],
)
def test_randomized_set(cls: type[stuff.RandomizedSet] | type[stuff.SlowRandomizedSet]):
    inst = cls()

    assert inst.insert(1) is True
    assert inst.remove(2) is False
    assert inst.insert(2) is True
    assert inst.getRandom() in (1, 2)
    assert inst.remove(1) is True
    assert inst.insert(2) is False
    assert inst.getRandom() == 2

    inst = cls()

    assert inst.insert(1) is True
    assert inst.insert(10) is True
    assert inst.insert(20) is True
    assert inst.insert(30) is True

    seen: set[int] = set()

    for _ in range(10_000):
        seen.add(inst.getRandom())

    assert seen == {1, 10, 20, 30}

    # ["remove","remove","insert","getRandom","remove","insert"]
    # [[0],[0],[0],[],[0],[0]]

    inst = cls()

    assert inst.remove(0) is False
    assert inst.remove(0) is False
    assert inst.insert(0) is True
    assert inst.getRandom() == 0
    assert inst.remove(0) is True
    assert inst.insert(0) is True

    # ["RandomizedSet","insert","insert","remove","insert","remove","getRandom"]
    # [[],[0],[1],[0],[2],[1],[]]

    inst = cls()

    assert inst.insert(0) is True
    assert inst.insert(1) is True
    assert inst.remove(0) is True
    assert inst.insert(2) is True
    assert inst.remove(1) is True
    assert inst.getRandom() == 2


def test_trie_single_letter():
    trie = stuff.Trie()

    assert trie.insert("a") is None
    assert trie.search("a") is True
    assert trie.startsWith("a") is True


def test_trie_prefix_leaf():
    trie = stuff.Trie()

    assert trie.insert("apple") is None
    assert trie.search("apple") is True
    assert trie.search("app") is False
    assert trie.startsWith("app") is True
    assert trie.insert("app") is None
    assert trie.search("app") is True


def test_trie_two_letter():
    trie = stuff.Trie()

    assert trie.insert("ab") is None
    assert trie.search("a") is False
    assert trie.startsWith("a") is True


def test_trie_busy():
    trie = stuff.Trie()

    assert trie.insert("app") is None
    assert trie.insert("apple") is None
    assert trie.insert("beer") is None
    assert trie.insert("add") is None
    assert trie.insert("jam") is None
    assert trie.insert("rental") is None
    assert trie.search("apps") is False
    assert trie.search("app") is True
    assert trie.search("ad") is False
    assert trie.search("applepie") is False
    assert trie.search("rest") is False
    assert trie.search("jan") is False
    assert trie.search("rent") is False
    assert trie.search("beer") is True
    assert trie.search("jam") is True
    assert trie.startsWith("apps") is False
    assert trie.startsWith("app") is True
    assert trie.startsWith("ad") is True
    assert trie.startsWith("applepie") is False
    assert trie.startsWith("rest") is False
    assert trie.startsWith("jan") is False
    assert trie.startsWith("rent") is True
    assert trie.startsWith("beer") is True
    assert trie.startsWith("jam") is True


@pytest.mark.parametrize(
    ("nums", "expected"),
    [
        (
            [-2, 1, -3, 4, -1, 2, 1, -5, 4],
            6,
        ),
        (
            [1],
            1,
        ),
        (
            [5, 4, -1, 7, 8],
            23,
        ),
        (
            [-2, 1],
            1,
        ),
        (
            json.load(open(".testdata/max_sub_array0.json")),
            11081,
        ),
    ],
)
def test_max_sub_array(nums: list[int], expected: int):
    assert stuff.sum_max_sub_array(nums) == expected


@pytest.mark.parametrize(
    ("inlist",),
    [
        ([[2, 3, 4], [1, 7], [1], [1, 5, 6, 8], [4], [4], [2], [4]],),
        ([[2, 4], [1, 3], [2, 4], [1, 3]],),
    ],
)
def test_copy_neighborly_node(inlist):
    orig = stuff.neighborly_node_from_list(inlist)
    copied = stuff.neighborly_node_from_list(stuff.neighborly_node_to_list(orig))

    assert id(orig) != id(copied)
    assert orig == copied
    assert stuff.neighborly_node_to_list(orig) == stuff.neighborly_node_to_list(copied)


@pytest.mark.parametrize(
    ("nums", "expected"),
    [
        ([3, 4, 5, 1, 2], 1),
        ([4, 5, 6, 7, 0, 1, 2], 0),
        ([11, 13, 15, 17], 11),
    ],
)
def test_find_min_in_rotated_array(nums: list[int], expected: int):
    assert stuff.find_min_in_rotated_array(nums) == expected
