import json
import typing
from typing import Dict, List, Set, Tuple

import pytest
from pandas import DataFrame

from reflex.base import Base
from reflex.state import BaseState
from reflex.vars import (
    BaseVar,
    ComputedVar,
    Var,
)

test_vars = [
    BaseVar(_var_name="prop1", _var_type=int),
    BaseVar(_var_name="key", _var_type=str),
    BaseVar(_var_name="value", _var_type=str)._var_set_state("state"),
    BaseVar(_var_name="local", _var_type=str, _var_is_local=True)._var_set_state(
        "state"
    ),
    BaseVar(_var_name="local2", _var_type=str, _var_is_local=True),
]


class ATestState(BaseState):
    """Test state."""

    value: str
    dict_val: Dict[str, List] = {}


@pytest.fixture
def TestObj():
    class TestObj(Base):
        foo: int
        bar: str

    return TestObj


@pytest.fixture
def ParentState(TestObj):
    class ParentState(BaseState):
        foo: int
        bar: int

        @ComputedVar
        def var_without_annotation(self):
            return TestObj

    return ParentState


@pytest.fixture
def ChildState(ParentState, TestObj):
    class ChildState(ParentState):
        @ComputedVar
        def var_without_annotation(self):
            return TestObj

    return ChildState


@pytest.fixture
def GrandChildState(ChildState, TestObj):
    class GrandChildState(ChildState):
        @ComputedVar
        def var_without_annotation(self):
            return TestObj

    return GrandChildState


@pytest.fixture
def StateWithAnyVar(TestObj):
    class StateWithAnyVar(BaseState):
        @ComputedVar
        def var_without_annotation(self) -> typing.Any:
            return TestObj

    return StateWithAnyVar


@pytest.fixture
def StateWithCorrectVarAnnotation():
    class StateWithCorrectVarAnnotation(BaseState):
        @ComputedVar
        def var_with_annotation(self) -> str:
            return "Correct annotation"

    return StateWithCorrectVarAnnotation


@pytest.fixture
def StateWithWrongVarAnnotation(TestObj):
    class StateWithWrongVarAnnotation(BaseState):
        @ComputedVar
        def var_with_annotation(self) -> str:
            return TestObj

    return StateWithWrongVarAnnotation


@pytest.mark.parametrize(
    "prop,expected",
    zip(
        test_vars,
        [
            "prop1",
            "key",
            "state.value",
            "state.local",
            "local2",
        ],
    ),
)
def test_full_name(prop, expected):
    """Test that the full name of a var is correct.

    Args:
        prop: The var to test.
        expected: The expected full name.
    """
    assert prop._var_full_name == expected


@pytest.mark.parametrize(
    "prop,expected",
    zip(
        test_vars,
        ["{prop1}", "{key}", "{state.value}", "state.local", "local2"],
    ),
)
def test_str(prop, expected):
    """Test that the string representation of a var is correct.

    Args:
        prop: The var to test.
        expected: The expected string representation.
    """
    assert str(prop) == expected


@pytest.mark.parametrize(
    "prop,expected",
    [
        (BaseVar(_var_name="p", _var_type=int), 0),
        (BaseVar(_var_name="p", _var_type=float), 0.0),
        (BaseVar(_var_name="p", _var_type=str), ""),
        (BaseVar(_var_name="p", _var_type=bool), False),
        (BaseVar(_var_name="p", _var_type=list), []),
        (BaseVar(_var_name="p", _var_type=dict), {}),
        (BaseVar(_var_name="p", _var_type=tuple), ()),
        (BaseVar(_var_name="p", _var_type=set), set()),
    ],
)
def test_default_value(prop, expected):
    """Test that the default value of a var is correct.

    Args:
        prop: The var to test.
        expected: The expected default value.
    """
    assert prop.get_default_value() == expected


@pytest.mark.parametrize(
    "prop,expected",
    zip(
        test_vars,
        [
            "set_prop1",
            "set_key",
            "state.set_value",
            "state.set_local",
            "set_local2",
        ],
    ),
)
def test_get_setter(prop, expected):
    """Test that the name of the setter function of a var is correct.

    Args:
        prop: The var to test.
        expected: The expected name of the setter function.
    """
    assert prop.get_setter_name() == expected


@pytest.mark.parametrize(
    "value,expected",
    [
        (None, None),
        (1, BaseVar(_var_name="1", _var_type=int, _var_is_local=True)),
        ("key", BaseVar(_var_name="key", _var_type=str, _var_is_local=True)),
        (3.14, BaseVar(_var_name="3.14", _var_type=float, _var_is_local=True)),
        ([1, 2, 3], BaseVar(_var_name="[1, 2, 3]", _var_type=list, _var_is_local=True)),
        (
            {"a": 1, "b": 2},
            BaseVar(_var_name='{"a": 1, "b": 2}', _var_type=dict, _var_is_local=True),
        ),
    ],
)
def test_create(value, expected):
    """Test the var create function.

    Args:
        value: The value to create a var from.
        expected: The expected name of the setter function.
    """
    prop = Var.create(value)
    if value is None:
        assert prop == expected
    else:
        assert prop.equals(expected)  # type: ignore


def test_create_type_error():
    """Test the var create function when inputs type error."""

    class ErrorType:
        pass

    value = ErrorType()

    with pytest.raises(TypeError):
        Var.create(value)


def v(value) -> Var:
    val = (
        Var.create(json.dumps(value), _var_is_string=True, _var_is_local=False)
        if isinstance(value, str)
        else Var.create(value, _var_is_local=False)
    )
    assert val is not None
    return val


def test_basic_operations(TestObj):
    """Test the var operations.

    Args:
        TestObj: The test object.
    """
    assert str(v(1) == v(2)) == "{((1) === (2))}"
    assert str(v(1) != v(2)) == "{((1) !== (2))}"
    assert str(v(1) < v(2)) == "{((1) < (2))}"
    assert str(v(1) <= v(2)) == "{((1) <= (2))}"
    assert str(v(1) > v(2)) == "{((1) > (2))}"
    assert str(v(1) >= v(2)) == "{((1) >= (2))}"
    assert str(v(1) + v(2)) == "{((1) + (2))}"
    assert str(v(1) - v(2)) == "{((1) - (2))}"
    assert str(v(1) * v(2)) == "{((1) * (2))}"
    assert str(v(1) / v(2)) == "{((1) / (2))}"
    assert str(v(1) // v(2)) == "{Math.floor((1) / (2))}"
    assert str(v(1) % v(2)) == "{((1) % (2))}"
    assert str(v(1) ** v(2)) == "{Math.pow((1) , (2))}"
    assert str(v(1) & v(2)) == "{((1) && (2))}"
    assert str(v(1) | v(2)) == "{((1) || (2))}"
    assert str(v([1, 2, 3])[v(0)]) == "{[1, 2, 3].at(0)}"
    assert str(v({"a": 1, "b": 2})["a"]) == '{{"a": 1, "b": 2}["a"]}'
    assert str(v("foo") == v("bar")) == '{(("foo") === ("bar"))}'
    assert (
        str(
            Var.create("foo", _var_is_local=False)
            == Var.create("bar", _var_is_local=False)
        )
        == "{((foo) === (bar))}"
    )
    assert (
        str(
            BaseVar(
                _var_name="foo", _var_type=str, _var_is_string=True, _var_is_local=True
            )
            == BaseVar(
                _var_name="bar", _var_type=str, _var_is_string=True, _var_is_local=True
            )
        )
        == "((`foo`) === (`bar`))"
    )
    assert (
        str(
            BaseVar(
                _var_name="foo",
                _var_type=TestObj,
                _var_is_string=True,
                _var_is_local=False,
            )
            ._var_set_state("state")
            .bar
            == BaseVar(
                _var_name="bar", _var_type=str, _var_is_string=True, _var_is_local=True
            )
        )
        == "{((state.foo.bar) === (`bar`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=TestObj)._var_set_state("state").bar)
        == "{state.foo.bar}"
    )
    assert str(abs(v(1))) == "{Math.abs(1)}"
    assert str(v([1, 2, 3]).length()) == "{[1, 2, 3].length}"
    assert str(v([1, 2]) + v([3, 4])) == "{spreadArraysOrObjects(([1, 2]) , ([3, 4]))}"

    # Tests for reverse operation
    assert str(v([1, 2, 3]).reverse()) == "{[...[1, 2, 3]].reverse()}"
    assert str(v(["1", "2", "3"]).reverse()) == '{[...["1", "2", "3"]].reverse()}'
    assert (
        str(BaseVar(_var_name="foo", _var_type=list)._var_set_state("state").reverse())
        == "{[...state.foo].reverse()}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=list).reverse())
        == "{[...foo].reverse()}"
    )
    assert str(BaseVar(_var_name="foo", _var_type=str)._type()) == "{typeof foo}"  # type: ignore
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() == str)  # type: ignore
        == "{((typeof foo) === (`string`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() == str)  # type: ignore
        == "{((typeof foo) === (`string`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() == int)  # type: ignore
        == "{((typeof foo) === (`number`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() == list)  # type: ignore
        == "{((typeof foo) === (`Array`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() == float)  # type: ignore
        == "{((typeof foo) === (`number`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() == tuple)  # type: ignore
        == "{((typeof foo) === (`Array`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() == dict)  # type: ignore
        == "{((typeof foo) === (`Object`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() != str)  # type: ignore
        == "{((typeof foo) !== (`string`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() != int)  # type: ignore
        == "{((typeof foo) !== (`number`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() != list)  # type: ignore
        == "{((typeof foo) !== (`Array`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() != float)  # type: ignore
        == "{((typeof foo) !== (`number`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() != tuple)  # type: ignore
        == "{((typeof foo) !== (`Array`))}"
    )
    assert (
        str(BaseVar(_var_name="foo", _var_type=str)._type() != dict)  # type: ignore
        == "{((typeof foo) !== (`Object`))}"
    )


@pytest.mark.parametrize(
    "var, expected",
    [
        (v([1, 2, 3]), "[1, 2, 3]"),
        (v(["1", "2", "3"]), '["1", "2", "3"]'),
        (BaseVar(_var_name="foo", _var_type=list)._var_set_state("state"), "state.foo"),
        (BaseVar(_var_name="foo", _var_type=list), "foo"),
        (v((1, 2, 3)), "[1, 2, 3]"),
        (v(("1", "2", "3")), '["1", "2", "3"]'),
        (
            BaseVar(_var_name="foo", _var_type=tuple)._var_set_state("state"),
            "state.foo",
        ),
        (BaseVar(_var_name="foo", _var_type=tuple), "foo"),
    ],
)
def test_list_tuple_contains(var, expected):
    assert str(var.contains(1)) == f"{{{expected}.includes(1)}}"
    assert str(var.contains("1")) == f'{{{expected}.includes("1")}}'
    assert str(var.contains(v(1))) == f"{{{expected}.includes(1)}}"
    assert str(var.contains(v("1"))) == f'{{{expected}.includes("1")}}'
    other_state_var = BaseVar(_var_name="other", _var_type=str)._var_set_state("state")
    other_var = BaseVar(_var_name="other", _var_type=str)
    assert str(var.contains(other_state_var)) == f"{{{expected}.includes(state.other)}}"
    assert str(var.contains(other_var)) == f"{{{expected}.includes(other)}}"


@pytest.mark.parametrize(
    "var, expected",
    [
        (v("123"), json.dumps("123")),
        (BaseVar(_var_name="foo", _var_type=str)._var_set_state("state"), "state.foo"),
        (BaseVar(_var_name="foo", _var_type=str), "foo"),
    ],
)
def test_str_contains(var, expected):
    assert str(var.contains("1")) == f'{{{expected}.includes("1")}}'
    assert str(var.contains(v("1"))) == f'{{{expected}.includes("1")}}'
    other_state_var = BaseVar(_var_name="other", _var_type=str)._var_set_state("state")
    other_var = BaseVar(_var_name="other", _var_type=str)
    assert str(var.contains(other_state_var)) == f"{{{expected}.includes(state.other)}}"
    assert str(var.contains(other_var)) == f"{{{expected}.includes(other)}}"


@pytest.mark.parametrize(
    "var, expected",
    [
        (v({"a": 1, "b": 2}), '{"a": 1, "b": 2}'),
        (BaseVar(_var_name="foo", _var_type=dict)._var_set_state("state"), "state.foo"),
        (BaseVar(_var_name="foo", _var_type=dict), "foo"),
    ],
)
def test_dict_contains(var, expected):
    assert str(var.contains(1)) == f"{{{expected}.hasOwnProperty(1)}}"
    assert str(var.contains("1")) == f'{{{expected}.hasOwnProperty("1")}}'
    assert str(var.contains(v(1))) == f"{{{expected}.hasOwnProperty(1)}}"
    assert str(var.contains(v("1"))) == f'{{{expected}.hasOwnProperty("1")}}'
    other_state_var = BaseVar(_var_name="other", _var_type=str)._var_set_state("state")
    other_var = BaseVar(_var_name="other", _var_type=str)
    assert (
        str(var.contains(other_state_var))
        == f"{{{expected}.hasOwnProperty(state.other)}}"
    )
    assert str(var.contains(other_var)) == f"{{{expected}.hasOwnProperty(other)}}"


@pytest.mark.parametrize(
    "var",
    [
        BaseVar(_var_name="list", _var_type=List[int]),
        BaseVar(_var_name="tuple", _var_type=Tuple[int, int]),
        BaseVar(_var_name="str", _var_type=str),
    ],
)
def test_var_indexing_lists(var):
    """Test that we can index into str, list or tuple vars.

    Args:
        var : The str, list or tuple base var.
    """
    # Test basic indexing.
    assert str(var[0]) == f"{{{var._var_name}.at(0)}}"
    assert str(var[1]) == f"{{{var._var_name}.at(1)}}"

    # Test negative indexing.
    assert str(var[-1]) == f"{{{var._var_name}.at(-1)}}"


def test_var_indexing_str():
    """Test that we can index into str vars."""
    str_var = BaseVar(_var_name="str", _var_type=str)

    # Test that indexing gives a type of Var[str].
    assert isinstance(str_var[0], Var)
    assert str_var[0]._var_type == str

    # Test basic indexing.
    assert str(str_var[0]) == "{str.at(0)}"
    assert str(str_var[1]) == "{str.at(1)}"

    # Test negative indexing.
    assert str(str_var[-1]) == "{str.at(-1)}"


@pytest.mark.parametrize(
    "var, index",
    [
        (BaseVar(_var_name="lst", _var_type=List[int]), [1, 2]),
        (BaseVar(_var_name="lst", _var_type=List[int]), {"name": "dict"}),
        (BaseVar(_var_name="lst", _var_type=List[int]), {"set"}),
        (
            BaseVar(_var_name="lst", _var_type=List[int]),
            (
                1,
                2,
            ),
        ),
        (BaseVar(_var_name="lst", _var_type=List[int]), 1.5),
        (BaseVar(_var_name="lst", _var_type=List[int]), "str"),
        (
            BaseVar(_var_name="lst", _var_type=List[int]),
            BaseVar(_var_name="string_var", _var_type=str),
        ),
        (
            BaseVar(_var_name="lst", _var_type=List[int]),
            BaseVar(_var_name="float_var", _var_type=float),
        ),
        (
            BaseVar(_var_name="lst", _var_type=List[int]),
            BaseVar(_var_name="list_var", _var_type=List[int]),
        ),
        (
            BaseVar(_var_name="lst", _var_type=List[int]),
            BaseVar(_var_name="set_var", _var_type=Set[str]),
        ),
        (
            BaseVar(_var_name="lst", _var_type=List[int]),
            BaseVar(_var_name="dict_var", _var_type=Dict[str, str]),
        ),
        (BaseVar(_var_name="str", _var_type=str), [1, 2]),
        (BaseVar(_var_name="lst", _var_type=str), {"name": "dict"}),
        (BaseVar(_var_name="lst", _var_type=str), {"set"}),
        (
            BaseVar(_var_name="lst", _var_type=str),
            BaseVar(_var_name="string_var", _var_type=str),
        ),
        (
            BaseVar(_var_name="lst", _var_type=str),
            BaseVar(_var_name="float_var", _var_type=float),
        ),
        (BaseVar(_var_name="str", _var_type=Tuple[str]), [1, 2]),
        (BaseVar(_var_name="lst", _var_type=Tuple[str]), {"name": "dict"}),
        (BaseVar(_var_name="lst", _var_type=Tuple[str]), {"set"}),
        (
            BaseVar(_var_name="lst", _var_type=Tuple[str]),
            BaseVar(_var_name="string_var", _var_type=str),
        ),
        (
            BaseVar(_var_name="lst", _var_type=Tuple[str]),
            BaseVar(_var_name="float_var", _var_type=float),
        ),
    ],
)
def test_var_unsupported_indexing_lists(var, index):
    """Test unsupported indexing throws a type error.

    Args:
        var: The base var.
        index: The base var index.
    """
    with pytest.raises(TypeError):
        var[index]


@pytest.mark.parametrize(
    "var",
    [
        BaseVar(_var_name="lst", _var_type=List[int]),
        BaseVar(_var_name="tuple", _var_type=Tuple[int, int]),
        BaseVar(_var_name="str", _var_type=str),
    ],
)
def test_var_list_slicing(var):
    """Test that we can slice into str, list or tuple vars.

    Args:
        var : The str, list or tuple base var.
    """
    assert str(var[:1]) == f"{{{var._var_name}.slice(0, 1)}}"
    assert str(var[:1]) == f"{{{var._var_name}.slice(0, 1)}}"
    assert str(var[:]) == f"{{{var._var_name}.slice(0, undefined)}}"


def test_dict_indexing():
    """Test that we can index into dict vars."""
    dct = BaseVar(_var_name="dct", _var_type=Dict[str, int])

    # Check correct indexing.
    assert str(dct["a"]) == '{dct["a"]}'
    assert str(dct["asdf"]) == '{dct["asdf"]}'


@pytest.mark.parametrize(
    "var, index",
    [
        (
            BaseVar(_var_name="dict", _var_type=Dict[str, str]),
            [1, 2],
        ),
        (
            BaseVar(_var_name="dict", _var_type=Dict[str, str]),
            {"name": "dict"},
        ),
        (
            BaseVar(_var_name="dict", _var_type=Dict[str, str]),
            {"set"},
        ),
        (
            BaseVar(_var_name="dict", _var_type=Dict[str, str]),
            (
                1,
                2,
            ),
        ),
        (
            BaseVar(_var_name="lst", _var_type=Dict[str, str]),
            BaseVar(_var_name="list_var", _var_type=List[int]),
        ),
        (
            BaseVar(_var_name="lst", _var_type=Dict[str, str]),
            BaseVar(_var_name="set_var", _var_type=Set[str]),
        ),
        (
            BaseVar(_var_name="lst", _var_type=Dict[str, str]),
            BaseVar(_var_name="dict_var", _var_type=Dict[str, str]),
        ),
        (
            BaseVar(_var_name="df", _var_type=DataFrame),
            [1, 2],
        ),
        (
            BaseVar(_var_name="df", _var_type=DataFrame),
            {"name": "dict"},
        ),
        (
            BaseVar(_var_name="df", _var_type=DataFrame),
            {"set"},
        ),
        (
            BaseVar(_var_name="df", _var_type=DataFrame),
            (
                1,
                2,
            ),
        ),
        (
            BaseVar(_var_name="df", _var_type=DataFrame),
            BaseVar(_var_name="list_var", _var_type=List[int]),
        ),
        (
            BaseVar(_var_name="df", _var_type=DataFrame),
            BaseVar(_var_name="set_var", _var_type=Set[str]),
        ),
        (
            BaseVar(_var_name="df", _var_type=DataFrame),
            BaseVar(_var_name="dict_var", _var_type=Dict[str, str]),
        ),
    ],
)
def test_var_unsupported_indexing_dicts(var, index):
    """Test unsupported indexing throws a type error.

    Args:
        var: The base var.
        index: The base var index.
    """
    with pytest.raises(TypeError):
        var[index]


@pytest.mark.parametrize(
    "fixture,full_name",
    [
        ("ParentState", "parent_state.var_without_annotation"),
        ("ChildState", "parent_state__child_state.var_without_annotation"),
        (
            "GrandChildState",
            "parent_state__child_state__grand_child_state.var_without_annotation",
        ),
        ("StateWithAnyVar", "state_with_any_var.var_without_annotation"),
    ],
)
def test_computed_var_without_annotation_error(request, fixture, full_name):
    """Test that a type error is thrown when an attribute of a computed var is
    accessed without annotating the computed var.

    Args:
        request: Fixture Request.
        fixture: The state fixture.
        full_name: The full name of the state var.
    """
    with pytest.raises(TypeError) as err:
        state = request.getfixturevalue(fixture)
        state.var_without_annotation.foo
    assert (
        err.value.args[0]
        == f"You must provide an annotation for the state var `{full_name}`. Annotation cannot be `typing.Any`"
    )


@pytest.mark.parametrize(
    "fixture,full_name",
    [
        (
            "StateWithCorrectVarAnnotation",
            "state_with_correct_var_annotation.var_with_annotation",
        ),
        (
            "StateWithWrongVarAnnotation",
            "state_with_wrong_var_annotation.var_with_annotation",
        ),
    ],
)
def test_computed_var_with_annotation_error(request, fixture, full_name):
    """Test that an Attribute error is thrown when a non-existent attribute of an annotated computed var is
    accessed or when the wrong annotation is provided to a computed var.

    Args:
        request: Fixture Request.
        fixture: The state fixture.
        full_name: The full name of the state var.
    """
    with pytest.raises(AttributeError) as err:
        state = request.getfixturevalue(fixture)
        state.var_with_annotation.foo
    assert (
        err.value.args[0]
        == f"The State var `{full_name}` has no attribute 'foo' or may have been annotated wrongly."
    )


@pytest.mark.parametrize(
    "out, expected",
    [
        (f"{BaseVar(_var_name='var', _var_type=str)}", "${var}"),
        (
            f"testing f-string with {BaseVar(_var_name='myvar', _var_type=int)._var_set_state('state')}",
            'testing f-string with $<reflex.Var>{"state": "state", "imports": {"/utils/context": [{"tag": "StateContexts", "is_default": false, "alias": null, "install": true, "render": true}], "react": [{"tag": "useContext", "is_default": false, "alias": null, "install": true, "render": true}]}, "hooks": ["const state = useContext(StateContexts.state)"]}</reflex.Var>{state.myvar}',
        ),
        (
            f"testing local f-string {BaseVar(_var_name='x', _var_is_local=True, _var_type=str)}",
            "testing local f-string x",
        ),
    ],
)
def test_fstrings(out, expected):
    assert out == expected


@pytest.mark.parametrize(
    ("value", "expect_state"),
    [
        ([1], ""),
        ({"a": 1}, ""),
        ([Var.create_safe(1)._var_set_state("foo")], "foo"),
        ({"a": Var.create_safe(1)._var_set_state("foo")}, "foo"),
    ],
)
def test_extract_state_from_container(value, expect_state):
    """Test that _var_state is extracted from containers containing BaseVar.

    Args:
        value: The value to create a var from.
        expect_state: The expected state.
    """
    assert Var.create_safe(value)._var_state == expect_state


@pytest.mark.parametrize(
    "value",
    [
        "var",
        "\nvar",
    ],
)
def test_fstring_roundtrip(value):
    """Test that f-string roundtrip carries state.

    Args:
        value: The value to create a Var from.
    """
    var = BaseVar.create_safe(value)._var_set_state("state")
    rt_var = Var.create_safe(f"{var}")
    assert var._var_state == rt_var._var_state
    assert var._var_full_name_needs_state_prefix
    assert not rt_var._var_full_name_needs_state_prefix
    assert rt_var._var_name == var._var_full_name


@pytest.mark.parametrize(
    "var",
    [
        BaseVar(_var_name="var", _var_type=int),
        BaseVar(_var_name="var", _var_type=float),
        BaseVar(_var_name="var", _var_type=str),
        BaseVar(_var_name="var", _var_type=bool),
        BaseVar(_var_name="var", _var_type=dict),
        BaseVar(_var_name="var", _var_type=tuple),
        BaseVar(_var_name="var", _var_type=set),
        BaseVar(_var_name="var", _var_type=None),
    ],
)
def test_unsupported_types_for_reverse(var):
    """Test that unsupported types for reverse throw a type error.

    Args:
        var: The base var.
    """
    with pytest.raises(TypeError) as err:
        var.reverse()
    assert err.value.args[0] == f"Cannot reverse non-list var var."


@pytest.mark.parametrize(
    "var",
    [
        BaseVar(_var_name="var", _var_type=int),
        BaseVar(_var_name="var", _var_type=float),
        BaseVar(_var_name="var", _var_type=bool),
        BaseVar(_var_name="var", _var_type=set),
        BaseVar(_var_name="var", _var_type=None),
    ],
)
def test_unsupported_types_for_contains(var):
    """Test that unsupported types for contains throw a type error.

    Args:
        var: The base var.
    """
    with pytest.raises(TypeError) as err:
        assert var.contains(1)
    assert (
        err.value.args[0]
        == f"Var var of type {var._var_type} does not support contains check."
    )


@pytest.mark.parametrize(
    "other",
    [
        BaseVar(_var_name="other", _var_type=int),
        BaseVar(_var_name="other", _var_type=float),
        BaseVar(_var_name="other", _var_type=bool),
        BaseVar(_var_name="other", _var_type=list),
        BaseVar(_var_name="other", _var_type=dict),
        BaseVar(_var_name="other", _var_type=tuple),
        BaseVar(_var_name="other", _var_type=set),
    ],
)
def test_unsupported_types_for_string_contains(other):
    with pytest.raises(TypeError) as err:
        assert BaseVar(_var_name="var", _var_type=str).contains(other)
    assert (
        err.value.args[0]
        == f"'in <string>' requires string as left operand, not {other._var_type}"
    )


def test_unsupported_default_contains():
    with pytest.raises(TypeError) as err:
        assert 1 in BaseVar(_var_name="var", _var_type=str)
    assert (
        err.value.args[0]
        == "'in' operator not supported for Var types, use Var.contains() instead."
    )


@pytest.mark.parametrize(
    "operand1_var,operand2_var,operators",
    [
        (
            Var.create(10),
            Var.create(5),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "&",
            ],
        ),
        (
            Var.create(10.5),
            Var.create(5),
            ["+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="],
        ),
        (
            Var.create(5),
            Var.create(True),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "&",
            ],
        ),
        (
            Var.create(10.5),
            Var.create(5.5),
            ["+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="],
        ),
        (
            Var.create(10.5),
            Var.create(True),
            ["+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="],
        ),
        (Var.create("10"), Var.create("5"), ["+", ">", "<", "<=", ">="]),
        (Var.create([10, 20]), Var.create([5, 6]), ["+", ">", "<", "<=", ">="]),
        (Var.create([10, 20]), Var.create(5), ["*"]),
        (Var.create([10, 20]), Var.create(True), ["*"]),
        (
            Var.create(True),
            Var.create(True),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "&",
            ],
        ),
    ],
)
def test_valid_var_operations(operand1_var: Var, operand2_var, operators: List[str]):
    """Test that operations do not raise a TypeError.

    Args:
        operand1_var: left operand.
        operand2_var: right operand.
        operators: list of supported operators.
    """
    for operator in operators:
        operand1_var.operation(op=operator, other=operand2_var)
        operand1_var.operation(op=operator, other=operand2_var, flip=True)


@pytest.mark.parametrize(
    "operand1_var,operand2_var,operators",
    [
        (
            Var.create(10),
            Var.create(5),
            [
                "^",
                "<<",
                ">>",
            ],
        ),
        (
            Var.create(10.5),
            Var.create(5),
            [
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create(10.5),
            Var.create(True),
            [
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create(10.5),
            Var.create(5.5),
            [
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create("10"),
            Var.create("5"),
            [
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create([10, 20]),
            Var.create([5, 6]),
            [
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create([10, 20]),
            Var.create(5),
            [
                "+",
                "-",
                "/",
                "//",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create([10, 20]),
            Var.create(True),
            [
                "+",
                "-",
                "/",
                "//",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create([10, 20]),
            Var.create("5"),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create([10, 20]),
            Var.create({"key": "value"}),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create([10, 20]),
            Var.create(5.5),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create({"key": "value"}),
            Var.create({"another_key": "another_value"}),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create({"key": "value"}),
            Var.create(5),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create({"key": "value"}),
            Var.create(True),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create({"key": "value"}),
            Var.create(5.5),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
        (
            Var.create({"key": "value"}),
            Var.create("5"),
            [
                "+",
                "-",
                "/",
                "//",
                "*",
                "%",
                "**",
                ">",
                "<",
                "<=",
                ">=",
                "|",
                "^",
                "<<",
                ">>",
                "&",
            ],
        ),
    ],
)
def test_invalid_var_operations(operand1_var: Var, operand2_var, operators: List[str]):
    for operator in operators:
        with pytest.raises(TypeError):
            operand1_var.operation(op=operator, other=operand2_var)

        with pytest.raises(TypeError):
            operand1_var.operation(op=operator, other=operand2_var, flip=True)


@pytest.mark.parametrize(
    "var, expected",
    [
        (Var.create("string_value", _var_is_string=True), "`string_value`"),
        (Var.create(1), "1"),
        (Var.create([1, 2, 3]), "[1, 2, 3]"),
        (Var.create({"foo": "bar"}), '{"foo": "bar"}'),
        (Var.create(ATestState.value, _var_is_string=True), "a_test_state.value"),
        (
            Var.create(f"{ATestState.value} string", _var_is_string=True),
            "`${a_test_state.value} string`",
        ),
        (Var.create(ATestState.dict_val), "a_test_state.dict_val"),
    ],
)
def test_var_name_unwrapped(var, expected):
    assert var._var_name_unwrapped == expected
