from typing import Tuple, List, Dict

import pytest
import redis
import valkey
from packaging.version import Version

from fakeredis._typing import ClientType
from test import testtools
from test.testtools import raw_command, resp_conversion, tuple_to_list

REDIS_VERSION = Version(redis.__version__)


def test_zadd_roman(r: ClientType):
    testtools.raw_command(r, "ZADD", "b", "0", "c")
    with pytest.raises(Exception) as ctx:
        testtools.raw_command(r, "SORT", "b")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    assert str(ctx.value) == "One or more scores can't be converted into double"


def test_zadd(r: ClientType):
    r.zadd("foo", {"four": 4})
    r.zadd("foo", {"three": 3})
    assert r.zadd("foo", {"two": 2, "one": 1, "zero": 0}) == 3
    assert r.zrange("foo", 0, -1) == [b"zero", b"one", b"two", b"three", b"four"]
    assert r.zadd("foo", {"zero": 7, "one": 1, "five": 5}) == 1
    assert r.zrange("foo", 0, -1) == [b"one", b"two", b"three", b"four", b"five", b"zero"]


def test_zadd_empty(r: ClientType):
    # Have to add at least one key/value pair
    with pytest.raises(Exception) as ctx:
        r.zadd("foo", {})
    assert isinstance(ctx.value, (redis.RedisError, valkey.ValkeyError))


@pytest.mark.supported_server_versions(min_redis_ver="7")
def test_zadd_minus_zero_redis7(r: ClientType):
    r.zadd("foo", {"a": -0.0})
    r.zadd("foo", {"a": 0.0})
    assert raw_command(r, "zscore", "foo", "a") == resp_conversion(r, 0.0, b"0")


def test_zadd_wrong_type(r: ClientType):
    r.sadd("foo", "bar")
    with pytest.raises(Exception) as exc_info:
        r.zadd("foo", {"two": 2})
    assert isinstance(exc_info.value, (redis.ResponseError, valkey.ResponseError))


def test_zadd_multiple(r: ClientType):
    r.zadd("foo", {"one": 1, "two": 2})
    assert r.zrange("foo", 0, 0) == [b"one"]
    assert r.zrange("foo", 1, 1) == [b"two"]


@pytest.mark.parametrize(
    ["map_to_add", "expected_ret", "expected_zrange_state"],
    [
        ({"four": 2.0, "three": 1.0}, 0, [(b"three", 3.0), (b"four", 4.0)]),
        ({"four": 2.0, "three": 1.0, "zero": 0.0}, 1, [(b"zero", 0.0), (b"three", 3.0), (b"four", 4.0)]),
        ({"two": 2.0, "one": 1.0}, 2, [(b"one", 1.0), (b"two", 2.0), (b"three", 3.0), (b"four", 4.0)]),
    ],
    ids=["no_additions", "partial_additions", "new_additions"],
)
@pytest.mark.parametrize("ch", [False, True])
def test_zadd_with_nx(
    r: ClientType,
    map_to_add: Dict[str, float],
    expected_ret: int,
    expected_zrange_state: List[Tuple[bytes, float]],
    ch: bool,
):
    r.zadd("foo", {"four": 4.0, "three": 3.0})
    assert r.zadd("foo", map_to_add, nx=True, ch=ch) == expected_ret
    assert r.zrange("foo", 0, -1, withscores=True) == resp_conversion(
        r, tuple_to_list(expected_zrange_state), expected_zrange_state
    )


@pytest.mark.parametrize(
    ["map_to_add", "expected_ret", "expected_zrange_state"],
    [
        ({"four": 2.0, "three": 1.0}, 0, [(b"three", 3.0), (b"four", 4.0)]),
        (
            {"four": 5.0, "three": 1.0, "zero": 0.0},
            2,
            [
                (b"zero", 0.0),
                (b"three", 3.0),
                (b"four", 5.0),
            ],
        ),
        ({"two": 2.0, "one": 1.0}, 2, [(b"one", 1.0), (b"two", 2.0), (b"three", 3.0), (b"four", 4.0)]),
    ],
    ids=["no_additions", "partial_additions", "new_additions"],
)
def test_zadd_with_gt_and_ch(
    r: ClientType, map_to_add: Dict[str, float], expected_ret: int, expected_zrange_state: List[Tuple[bytes, float]]
):
    r.zadd("foo", {"four": 4.0, "three": 3.0})
    assert r.zadd("foo", map_to_add, gt=True, ch=True) == expected_ret
    assert r.zrange("foo", 0, -1, withscores=True) == resp_conversion(
        r, tuple_to_list(expected_zrange_state), expected_zrange_state
    )


@pytest.mark.parametrize(
    ["map_to_add", "expected_ret", "expected_zrange_state"],
    [
        ({"four": 2.0, "three": 1.0}, 0, [(b"three", 3.0), (b"four", 4.0)]),
        ({"four": 5.0, "three": 1.0, "zero": 0.0}, 1, [(b"zero", 0.0), (b"three", 3.0), (b"four", 5.0)]),
        ({"two": 2.0, "one": 1.0}, 2, [(b"one", 1.0), (b"two", 2.0), (b"three", 3.0), (b"four", 4.0)]),
    ],
    ids=["no_additions", "partial_additions", "new_additions"],
)
def test_zadd_with_gt(
    r: ClientType, map_to_add: Dict[str, float], expected_ret: int, expected_zrange_state: List[Tuple[bytes, float]]
):
    r.zadd("foo", {"four": 4.0, "three": 3.0})
    assert r.zadd("foo", map_to_add, gt=True) == expected_ret
    assert r.zrange("foo", 0, -1, withscores=True) == resp_conversion(
        r, tuple_to_list(expected_zrange_state), expected_zrange_state
    )


@pytest.mark.parametrize(
    ["map_to_add", "expected_ret", "expected_zrange_state"],
    [
        ({"four": 4.0, "three": 1.0}, 1, [(b"three", 1.0), (b"four", 4.0)]),
        ({"four": 4.0, "three": 1.0, "zero": 0.0}, 2, [(b"zero", 0.0), (b"three", 1.0), (b"four", 4.0)]),
        ({"two": 2.0, "one": 1.0}, 2, [(b"one", 1.0), (b"two", 2.0), (b"three", 3.0), (b"four", 4.0)]),
    ],
    ids=["no_additions", "partial_additions", "new_additions"],
)
def test_zadd_with_ch(
    r: ClientType, map_to_add: Dict[str, float], expected_ret: int, expected_zrange_state: List[Tuple[bytes, float]]
):
    r.zadd("foo", {"four": 4.0, "three": 3.0})
    assert r.zadd("foo", map_to_add, ch=True) == expected_ret
    assert r.zrange("foo", 0, -1, withscores=True) == resp_conversion(
        r, tuple_to_list(expected_zrange_state), expected_zrange_state
    )


@pytest.mark.parametrize(
    ["map_to_add", "expected_ret", "expected_zrange_state"],
    [
        ({"four": 2.0, "three": 1.0}, 2, [(b"three", 1.0), (b"four", 2.0)]),
        ({"four": 4.0, "three": 3.0, "zero": 0.0}, 0, [(b"three", 3.0), (b"four", 4.0)]),
        ({"two": 2.0, "one": 1.0}, 0, [(b"three", 3.0), (b"four", 4.0)]),
    ],
    ids=["new_additions", "no_additions", "no_additions_2"],
)
@pytest.mark.parametrize("ch", [False, True])
def test_zadd_with_xx(
    r: ClientType,
    map_to_add: Dict[str, float],
    expected_ret: int,
    expected_zrange_state: List[Tuple[bytes, float]],
    ch: bool,
):
    r.zadd("foo", {"four": 4.0, "three": 3.0})
    assert r.zadd("foo", map_to_add, xx=True, ch=ch) == (expected_ret if ch else 0)
    assert r.zrange("foo", 0, -1, withscores=True) == resp_conversion(
        r, tuple_to_list(expected_zrange_state), expected_zrange_state
    )


@pytest.mark.parametrize("ch", [False, True])
def test_zadd_with_nx_and_xx(r: ClientType, ch: bool):
    r.zadd("foo", {"four": 4.0, "three": 3.0})
    with pytest.raises(Exception) as ctx:
        r.zadd("foo", {"four": -4.0, "three": -3.0}, nx=True, xx=True, ch=ch)
    assert isinstance(ctx.value, (redis.DataError, valkey.DataError))


@pytest.mark.parametrize("ch", [False, True])
def test_zadd_incr(r: ClientType, ch: bool):
    r.zadd("foo", {"four": 4.0, "three": 3.0})
    assert r.zadd("foo", {"four": 1.0}, incr=True, ch=ch) == 5.0
    assert r.zadd("foo", {"three": 1.0}, incr=True, nx=True, ch=ch) is None
    assert r.zscore("foo", "three") == 3.0
    assert r.zadd("foo", {"bar": 1.0}, incr=True, xx=True, ch=ch) is None
    assert r.zadd("foo", {"three": 1.0}, incr=True, xx=True, ch=ch) == 4.0


def test_zadd_incr_with_gt_and_lt(r: ClientType):
    r.zadd("foo", {"m": 5.0})
    # GT + INCR: a decrement makes the new score not greater -> aborted, returns None, score unchanged
    assert r.zadd("foo", {"m": -1.0}, incr=True, gt=True) is None
    assert r.zscore("foo", "m") == 5.0
    # GT + INCR: an increment makes the new score greater -> applied
    assert r.zadd("foo", {"m": 3.0}, incr=True, gt=True) == 8.0
    # LT + INCR: an increment makes the new score not less -> aborted
    assert r.zadd("foo", {"m": 1.0}, incr=True, lt=True) is None
    assert r.zscore("foo", "m") == 8.0
    # LT + INCR: a decrement makes the new score less -> applied
    assert r.zadd("foo", {"m": -2.0}, incr=True, lt=True) == 6.0
    # GT/LT do not prevent adding a brand-new member
    assert r.zadd("foo", {"new": 2.0}, incr=True, gt=True) == 2.0
    assert r.zscore("foo", "new") == 2.0


def test_zadd_with_xx_and_gt_and_ch(r: ClientType):
    r.zadd("test", {"one": 1})
    assert r.zscore("test", "one") == 1.0
    assert r.zadd("test", {"one": 4}, xx=True, gt=True, ch=True) == 1
    assert r.zscore("test", "one") == 4.0
    assert r.zadd("test", {"one": 0}, xx=True, gt=True, ch=True) == 0
    assert r.zscore("test", "one") == 4.0


def test_zadd_and_zrangebyscore(r: ClientType):
    raw_command(r, "zadd", "", 0.0, "")
    assert raw_command(r, "zrangebyscore", "", 0.0, 0.0, "limit", 0, 0) == []
    with pytest.raises(Exception) as exc_info:
        raw_command(r, "zrangebyscore", "", 0.0, 0.0, "limit", 0)
    assert isinstance(exc_info.value, (redis.RedisError, valkey.ValkeyError))
    with pytest.raises(Exception) as exc_info:
        raw_command(r, "zadd", "t", 0.0, "xx", "")
    assert isinstance(exc_info.value, (redis.RedisError, valkey.ValkeyError))
