from __future__ import annotations

import os
from datetime import timedelta
from time import sleep

import pytest
import redis
import valkey

from fakeredis._typing import ClientType
from test import testtools


@testtools.run_test_if_redispy_ver("gte", "5.1")
def test_sadd(r: ClientType):
    assert r.sadd("foo", "member1") == 1
    assert r.sadd("foo", "member1") == 0
    assert set(r.smembers("foo")) == {b"member1"}
    assert r.sadd("foo", "member2", "member3") == 2
    assert set(r.smembers("foo")) == {b"member1", b"member2", b"member3"}
    assert r.sadd("foo", "member3", "member4") == 1
    assert set(r.smembers("foo")) == {b"member1", b"member2", b"member3", b"member4"}


@testtools.run_test_if_redispy_ver("lt", "5.1")
def test_sadd_redispy_5(r: ClientType):
    assert r.sadd("foo", "member1") == 1
    assert r.sadd("foo", "member1") == 0
    assert r.smembers("foo") == {b"member1"}
    assert r.sadd("foo", "member2", "member3") == 2
    assert r.smembers("foo") == {b"member1", b"member2", b"member3"}
    assert r.sadd("foo", "member3", "member4") == 1
    assert r.smembers("foo") == {b"member1", b"member2", b"member3", b"member4"}


def test_sadd_as_str_type(r: ClientType):
    assert r.sadd("foo", *range(3)) == 3
    assert set(r.smembers("foo")) == {b"0", b"1", b"2"}


def test_sadd_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.sadd("foo", "member2")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_scard(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("foo", "member2")
    assert r.scard("foo") == 2


def test_scard_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.scard("foo")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_sdiff(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert set(r.sdiff("foo", "bar")) == {b"member1"}
    # Original sets shouldn't be modified.
    assert set(r.smembers("foo")) == {b"member1", b"member2"}
    assert set(r.smembers("bar")) == {b"member2", b"member3"}


def test_sdiff_one_key(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    assert set(r.sdiff("foo")) == {b"member1", b"member2"}


def test_sdiff_empty(r: ClientType):
    assert set(r.sdiff("foo")) == set()


def test_sdiff_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    r.sadd("bar", "member")
    with pytest.raises(Exception) as ctx:
        r.sdiff("foo", "bar")
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    with pytest.raises(Exception) as ctx:
        r.sdiff("bar", "foo")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_sdiffstore(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert r.sdiffstore("baz", "foo", "bar") == 1

    # Catch instances where we store bytes and strings inconsistently
    # and thus baz = {'member1', b'member1'}
    r.sadd("baz", "member1")
    assert r.scard("baz") == 1


def test_sinter(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert set(r.sinter("foo", "bar")) == {b"member2"}
    assert set(r.sinter("foo")) == {b"member1", b"member2"}


def test_sinter_bytes_keys(r: ClientType):
    foo = os.urandom(10)
    bar = os.urandom(10)
    r.sadd(foo, "member1")
    r.sadd(foo, "member2")
    r.sadd(bar, "member2")
    r.sadd(bar, "member3")
    assert set(r.sinter(foo, bar)) == {b"member2"}
    assert set(r.sinter(foo)) == {b"member1", b"member2"}


def test_sinter_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    r.sadd("bar", "member")
    with pytest.raises(Exception) as ctx:
        r.sinter("foo", "bar")
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    with pytest.raises(Exception) as ctx:
        r.sinter("bar", "foo")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_sinterstore(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert r.sinterstore("baz", "foo", "bar") == 1

    # Catch instances where we store bytes and strings inconsistently
    # and thus baz = {'member2', b'member2'}
    r.sadd("baz", "member2")
    assert r.scard("baz") == 1


def test_sismember(r: ClientType):
    assert not r.sismember("foo", "member1")
    r.sadd("foo", "member1")
    assert r.sismember("foo", "member1")


def test_smismember(r: ClientType):
    assert r.smismember("foo", ["member1", "member2", "member3"]) == [0, 0, 0]
    r.sadd("foo", "member1", "member2", "member3")
    assert r.smismember("foo", ["member1", "member2", "member3"]) == [1, 1, 1]
    assert r.smismember("foo", ["member1", "member2", "member3", "member4"]) == [1, 1, 1, 0]
    assert r.smismember("foo", ["member4", "member2", "member3"]) == [0, 1, 1]
    # should also work if provided values as arguments
    assert r.smismember("foo", "member4", "member2", "member3") == [0, 1, 1]


def test_smismember_wrong_type(r: ClientType):
    # verify that command fails when the key itself is not a SET
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.smismember("foo", "member")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    # verify that command fails if the input parameter is of wrong type
    r.sadd("foo2", "member1", "member2", "member3")
    with pytest.raises(Exception, match="Invalid input of type") as ctx:
        r.smismember("foo2", [["member1", "member2"]])

    assert isinstance(ctx.value, (redis.DataError, valkey.DataError))


def test_sismember_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.sismember("foo", "member")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_smembers(r: ClientType):
    assert set(r.smembers("foo")) == set()


def test_smembers_copy(r: ClientType):
    r.sadd("foo", "member1")
    ret = r.smembers("foo")
    r.sadd("foo", "member2")
    assert r.smembers("foo") != ret


def test_smembers_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.smembers("foo")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_smembers_runtime_error(r: ClientType):
    r.sadd("foo", "member1", "member2")
    for member in r.smembers("foo"):
        r.srem("foo", member)


def test_smove(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    assert r.smove("foo", "bar", "member1")
    assert set(r.smembers("bar")) == {b"member1"}


def test_smove_non_existent_key(r: ClientType):
    assert not r.smove("foo", "bar", "member1")


def test_smove_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    r.sadd("bar", "member")
    with pytest.raises(Exception) as ctx:
        r.smove("bar", "foo", "member")
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    # Must raise the error before removing member from bar
    assert set(r.smembers("bar")) == {b"member"}
    with pytest.raises(Exception) as ctx:
        r.smove("foo", "bar", "member")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_spop(r: ClientType):
    # This is tricky because it pops a random element.
    r.sadd("foo", "member1")
    assert r.spop("foo") == b"member1"
    assert r.spop("foo") is None


def test_spop_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.spop("foo")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_srandmember(r: ClientType):
    r.sadd("foo", "member1")
    assert r.srandmember("foo") == b"member1"
    # Shouldn't be removed from the set.
    assert r.srandmember("foo") == b"member1"


def test_srandmember_number(r: ClientType):
    """srandmember works with the number argument."""
    assert r.srandmember("foo", 2) == []
    r.sadd("foo", b"member1")
    assert r.srandmember("foo", 2) == [b"member1"]
    r.sadd("foo", b"member2")
    assert set(r.srandmember("foo", 2)) == {b"member1", b"member2"}
    r.sadd("foo", b"member3")
    res = r.srandmember("foo", 2)
    assert len(res) == 2
    for e in res:
        assert e in {b"member1", b"member2", b"member3"}


def test_srandmember_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.srandmember("foo")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_srem(r: ClientType):
    r.sadd("foo", "member1", "member2", "member3", "member4")
    assert set(r.smembers("foo")) == {b"member1", b"member2", b"member3", b"member4"}
    assert r.srem("foo", "member1") == 1
    assert set(r.smembers("foo")) == {b"member2", b"member3", b"member4"}
    assert r.srem("foo", "member1") == 0
    # Since redis>=2.7.6 returns number of deleted items.
    assert r.srem("foo", "member2", "member3") == 2
    assert set(r.smembers("foo")) == {b"member4"}
    assert r.srem("foo", "member3", "member4") == 1
    assert set(r.smembers("foo")) == set()
    assert r.srem("foo", "member3", "member4") == 0


def test_srem_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    with pytest.raises(Exception) as ctx:
        r.srem("foo", "member")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_sunion(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert set(r.sunion("foo", "bar")) == {b"member1", b"member2", b"member3"}


def test_sunion_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    r.sadd("bar", "member")
    with pytest.raises(Exception) as ctx:
        r.sunion("foo", "bar")
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    with pytest.raises(Exception) as ctx:
        r.sunion("bar", "foo")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_sunionstore(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert r.sunionstore("baz", "foo", "bar") == 3
    assert set(r.smembers("baz")) == {b"member1", b"member2", b"member3"}

    # Catch instances where we store bytes and strings inconsistently
    # and thus baz = {b'member1', b'member2', b'member3', 'member3'}
    r.sadd("baz", "member3")
    assert r.scard("baz") == 3


def test_empty_set(r: ClientType):
    r.sadd("foo", "bar")
    r.srem("foo", "bar")
    assert not r.exists("foo")


def test_sscan(r: ClientType):
    # Set up the data
    name = "sscan-test"
    for ix in range(20):
        k = "sscan-test:%s" % ix
        r.sadd(name, k)
    expected = r.smembers(name)
    assert len(expected) == 20  # Ensure we know what we're testing

    # Test that we page through the results and get everything out
    results = []
    cursor = "0"
    while cursor != 0:
        cursor, data = r.sscan(name, cursor, count=6)
        results.extend(data)
    assert set(expected) == set(results)

    # Test the iterator version
    results = list(r.sscan_iter(name, count=6))
    assert set(expected) == set(results)

    # Now test that the MATCH functionality works
    results = []
    cursor = "0"
    while cursor != 0:
        cursor, data = r.sscan(name, cursor, match="*7", count=100)
        results.extend(data)
    assert b"sscan-test:7" in results
    assert b"sscan-test:17" in results
    assert len(results) == 2

    # Test the match on iterator
    results = list(r.sscan_iter(name, match="*7"))
    assert b"sscan-test:7" in results
    assert b"sscan-test:17" in results
    assert len(results) == 2


@pytest.mark.supported_server_versions(min_redis_ver="7")
def test_sintercard(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert r.sintercard(2, ["foo", "bar"]) == 1
    assert r.sintercard(1, ["foo"]) == 2


@pytest.mark.supported_server_versions(min_redis_ver="7")
def test_sintercard_key_doesnt_exist(r: ClientType):
    r.sadd("foo", "member1")
    r.sadd("foo", "member2")
    r.sadd("bar", "member2")
    r.sadd("bar", "member3")
    assert r.sintercard(2, ["foo", "bar"]) == 1
    assert r.sintercard(1, ["foo"]) == 2
    assert r.sintercard(1, ["foo"], limit=1) == 1
    assert r.sintercard(3, ["foo", "bar", "ddd"]) == 0


@pytest.mark.supported_server_versions(min_redis_ver="7")
def test_sintercard_bytes_keys(r: ClientType):
    foo = os.urandom(10)
    bar = os.urandom(10)
    r.sadd(foo, "member1")
    r.sadd(foo, "member2")
    r.sadd(bar, "member2")
    r.sadd(bar, "member3")
    assert r.sintercard(2, [foo, bar]) == 1
    assert r.sintercard(1, [foo]) == 2
    assert r.sintercard(1, [foo], limit=1) == 1


@pytest.mark.supported_server_versions(min_redis_ver="7")
def test_sintercard_negative_limit(r: ClientType):
    r.sadd("foo", "member1", "member2")
    r.sadd("bar", "member2", "member3")
    with pytest.raises(Exception) as ctx:
        r.sintercard(2, ["foo", "bar"], limit=-1)
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    assert "LIMIT can't be negative" in str(ctx.value)


@pytest.mark.supported_server_versions(min_redis_ver="7")
def test_sintercard_wrong_type(r: ClientType):
    r.zadd("foo", {"member": 1})
    r.sadd("bar", "member")
    with pytest.raises(Exception) as ctx:
        r.sintercard(2, ["foo", "bar"])
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    with pytest.raises(Exception) as ctx:
        r.sintercard(2, ["bar", "foo"])

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


@pytest.mark.supported_server_versions(min_redis_ver="7")
def test_sintercard_syntax_error(r: ClientType):
    r.zadd("foo", {"member": 1})
    r.sadd("bar", "member")
    with pytest.raises(Exception) as ctx:
        r.sintercard(3, ["foo", "bar"])
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    with pytest.raises(Exception) as ctx:
        r.sintercard(1, ["bar", "foo"])
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    with pytest.raises(Exception) as ctx:
        r.sintercard(1, ["bar", "foo"], limit="x")

    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))


def test_pfadd(r: ClientType):
    key = "hll-pfadd"
    assert r.pfadd(key, "a", "b", "c", "d", "e", "f", "g") == 1
    assert r.pfcount(key) == 7


def test_pfcount(r: ClientType):
    key1 = "hll-pfcount01"
    key2 = "hll-pfcount02"
    key3 = "hll-pfcount03"
    assert r.pfadd(key1, "foo", "bar", "zap") == 1
    assert r.pfadd(key1, "zap", "zap", "zap") == 0
    assert r.pfadd(key1, "foo", "bar") == 0
    assert r.pfcount(key1) == 3
    assert r.pfadd(key2, "1", "2", "3") == 1
    assert r.pfcount(key2) == 3
    assert r.pfcount(key1, key2) == 6
    assert r.pfadd(key3, "foo", "bar", "zip") == 1
    assert r.pfcount(key3) == 3
    assert r.pfcount(key1, key3) == 4
    assert r.pfcount(key1, key2, key3) == 7


def test_pfmerge(r: ClientType):
    key1 = "hll-pfmerge01"
    key2 = "hll-pfmerge02"
    key3 = "hll-pfmerge03"
    assert r.pfadd(key1, "foo", "bar", "zap", "a") == 1
    assert r.pfadd(key2, "a", "b", "c", "foo") == 1
    assert r.pfmerge(key3, key1, key2)
    assert r.pfcount(key3) == 6


@pytest.mark.slow
def test_set_ex_should_expire_value(r: ClientType):
    r.set("foo", "bar")
    assert r.get("foo") == b"bar"
    r.set("foo", "bar", ex=1)
    sleep(2)
    assert r.get("foo") is None


@pytest.mark.slow
def test_set_px_should_expire_value(r: ClientType):
    r.set("foo", "bar", px=500)
    sleep(1.5)
    assert r.get("foo") is None


@pytest.mark.slow
def test_psetex_expire_value(r: ClientType):
    with pytest.raises(Exception) as ctx:
        r.psetex("foo", 0, "bar")
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    r.psetex("foo", 500, "bar")
    sleep(1.5)
    assert r.get("foo") is None


@pytest.mark.slow
def test_psetex_expire_value_using_timedelta(r: ClientType):
    with pytest.raises(Exception) as ctx:
        r.psetex("foo", timedelta(seconds=0), "bar")
    assert isinstance(ctx.value, (redis.ResponseError, valkey.ResponseError))
    r.psetex("foo", timedelta(seconds=0.5), "bar")
    sleep(1.5)
    assert r.get("foo") is None
