using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using StackExchange.Redis;
using Xunit;
using Xunit.Abstractions;
using System.Threading.Tasks;

namespace StackExchange.Redis.Tests.Booksleeve
{
    public class Hashes : BookSleeveTestBase // https://redis.io/commands#hash
    {
        public Hashes(ITestOutputHelper output) : base(output) { }

        [Fact]
        public async Task TestIncrBy()
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(5);
                await conn.KeyDeleteAsync("hash-test").ForAwait();
                for (int i = 1; i < 1000; i++)
                {
                    Assert.Equal(i, await conn.HashIncrementAsync("hash-test", "a", 1).ForAwait());
                    Assert.Equal(-i, await conn.HashIncrementAsync("hash-test", "b", -1).ForAwait());
                    //Assert.Equal(i, conn.Wait(conn.Hashes.Increment(5, "hash-test", "a", 1)));
                    //Assert.Equal(-i, conn.Wait(conn.Hashes.Increment(5, "hash-test", "b", -1)));
                }
            }
        }

        [Fact]
        public void Scan()
        {
            using (var muxer = GetUnsecuredConnection(waitForOpen: true))
            {
                Skip.IfMissingFeature(muxer, nameof(RedisFeatures.Scan), r => r.Scan);

                const int db = 3;
                var conn = muxer.GetDatabase(db);

                const string key = "hash-scan";
                conn.KeyDeleteAsync(key);
                conn.HashSetAsync(key, "abc", "def");
                conn.HashSetAsync(key, "ghi", "jkl");
                conn.HashSetAsync(key, "mno", "pqr");

                var t1 = conn.HashScan(key);
                var t2 = conn.HashScan(key, "*h*");
                var t3 = conn.HashScan(key);
                var t4 = conn.HashScan(key, "*h*");

                var v1 = t1.ToArray();
                var v2 = t2.ToArray();
                var v3 = t3.ToArray();
                var v4 = t4.ToArray();

                Assert.Equal(3, v1.Length);
                Assert.Single(v2);
                Assert.Equal(3, v3.Length);
                Assert.Single(v4);
                Array.Sort(v1, (x, y) => string.Compare(x.Name, y.Name));
                Array.Sort(v2, (x, y) => string.Compare(x.Name, y.Name));
                Array.Sort(v3, (x, y) => string.Compare(x.Name, y.Name));
                Array.Sort(v4, (x, y) => string.Compare(x.Name, y.Name));

                Assert.Equal("abc=def,ghi=jkl,mno=pqr", string.Join(",", v1.Select(pair => pair.Name + "=" + pair.Value)));
                Assert.Equal("ghi=jkl", string.Join(",", v2.Select(pair => pair.Name + "=" + pair.Value)));
                Assert.Equal("abc=def,ghi=jkl,mno=pqr", string.Join(",", v3.Select(pair => pair.Name + "=" + pair.Value)));
                Assert.Equal("ghi=jkl", string.Join(",", v4.Select(pair => pair.Name + "=" + pair.Value)));
            }
        }

        [Fact]
        public void TestIncrementOnHashThatDoesntExist()
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(0);
                conn.KeyDeleteAsync("keynotexist");
                var result1 = conn.Wait(conn.HashIncrementAsync("keynotexist", "fieldnotexist", 1));
                var result2 = conn.Wait(conn.HashIncrementAsync("keynotexist", "anotherfieldnotexist", 1));
                Assert.Equal(1, result1);
                Assert.Equal(1, result2);
            }
        }

        [Fact]
        public async Task TestIncrByFloat()
        {
            using (var muxer = GetUnsecuredConnection(waitForOpen: true))
            {
                Skip.IfMissingFeature(muxer, nameof(RedisFeatures.IncrementFloat), r => r.IncrementFloat);
                var conn = muxer.GetDatabase(5);
                await conn.KeyDeleteAsync("hash-test").ForAwait();
                for (int i = 1; i < 1000; i++)
                {
                    Assert.Equal(i, await conn.HashIncrementAsync("hash-test", "a", 1.0).ForAwait());
                    Assert.Equal(-i, await conn.HashIncrementAsync("hash-test", "b", -1.0).ForAwait());
                }
            }
        }

        [Fact]
        public async Task TestGetAll()
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(6);
                const string key = "hash test";
                await conn.KeyDeleteAsync(key).ForAwait();
                var shouldMatch = new Dictionary<Guid, int>();
                var random = new Random();

                for (int i = 1; i < 1000; i++)
                {
                    var guid = Guid.NewGuid();
                    var value = random.Next(int.MaxValue);

                    shouldMatch[guid] = value;

                    var x = await conn.HashIncrementAsync(key, guid.ToString(), value).ForAwait();
                }
                var inRedis = (await conn.HashGetAllAsync(key).ForAwait()).ToDictionary(
                    x => Guid.Parse(x.Name), x => int.Parse(x.Value));

                Assert.Equal(shouldMatch.Count, inRedis.Count);

                foreach (var k in shouldMatch.Keys)
                {
                    Assert.Equal(shouldMatch[k], inRedis[k]);
                }
            }
        }

        [Fact]
        public async Task TestGet()
        {
            using (var muxer = GetUnsecuredConnection())
            {
                const string key = "hash test";
                var conn = muxer.GetDatabase(6);
                var shouldMatch = new Dictionary<Guid, int>();
                var random = new Random();

                for (int i = 1; i < 1000; i++)
                {
                    var guid = Guid.NewGuid();
                    var value = random.Next(int.MaxValue);

                    shouldMatch[guid] = value;

                    var x = await conn.HashIncrementAsync(key, guid.ToString(), value).ForAwait();
                }

                foreach (var k in shouldMatch.Keys)
                {
                    var inRedis = await conn.HashGetAsync(key, k.ToString()).ForAwait();
                    var num = int.Parse(inRedis);

                    Assert.Equal(shouldMatch[k], num);
                }
            }
        }

        [Fact]
        public async Task TestSet() // https://redis.io/commands/hset
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                var val0 = conn.HashGetAsync("hashkey", "field").ForAwait();
                var set0 = conn.HashSetAsync("hashkey", "field", "value1").ForAwait();
                var val1 = conn.HashGetAsync("hashkey", "field").ForAwait();
                var set1 = conn.HashSetAsync("hashkey", "field", "value2").ForAwait();
                var val2 = conn.HashGetAsync("hashkey", "field").ForAwait();

                var set2 = conn.HashSetAsync("hashkey", "field-blob", Encoding.UTF8.GetBytes("value3")).ForAwait();
                var val3 = conn.HashGetAsync("hashkey", "field-blob").ForAwait();

                var set3 = conn.HashSetAsync("hashkey", "empty_type1", "").ForAwait();
                var val4 = conn.HashGetAsync("hashkey", "empty_type1").ForAwait();
                var set4 = conn.HashSetAsync("hashkey", "empty_type2", RedisValue.EmptyString).ForAwait();
                var val5 = conn.HashGetAsync("hashkey", "empty_type2").ForAwait();

                Assert.Null((string)(await val0));
                Assert.True(await set0);
                Assert.Equal("value1", await val1);
                Assert.False(await set1);
                Assert.Equal("value2", await val2);

                Assert.True(await set2);
                Assert.Equal("value3", await val3);

                Assert.True(await set3);
                Assert.Equal("", await val4);
                Assert.True(await set4);
                Assert.Equal("", await val5);
            }
        }

        [Fact]
        public async Task TestSetNotExists() // https://redis.io/commands/hsetnx
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                var val0 = conn.HashGetAsync("hashkey", "field").ForAwait();
                var set0 = conn.HashSetAsync("hashkey", "field", "value1", When.NotExists).ForAwait();
                var val1 = conn.HashGetAsync("hashkey", "field").ForAwait();
                var set1 = conn.HashSetAsync("hashkey", "field", "value2", When.NotExists).ForAwait();
                var val2 = conn.HashGetAsync("hashkey", "field").ForAwait();

                var set2 = conn.HashSetAsync("hashkey", "field-blob", Encoding.UTF8.GetBytes("value3"), When.NotExists).ForAwait();
                var val3 = conn.HashGetAsync("hashkey", "field-blob").ForAwait();
                var set3 = conn.HashSetAsync("hashkey", "field-blob", Encoding.UTF8.GetBytes("value3"), When.NotExists).ForAwait();

                Assert.Null((string)(await val0));
                Assert.True(await set0);
                Assert.Equal("value1", await val1);
                Assert.False(await set1);
                Assert.Equal("value1", await val2);

                Assert.True(await set2);
                Assert.Equal("value3", await val3);
                Assert.False(await set3);
            }
        }

        [Fact]
        public async Task TestDelSingle() // https://redis.io/commands/hdel
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();
                var del0 = conn.HashDeleteAsync("hashkey", "field").ForAwait();

                await conn.HashSetAsync("hashkey", "field", "value").ForAwait();

                var del1 = conn.HashDeleteAsync("hashkey", "field").ForAwait();
                var del2 = conn.HashDeleteAsync("hashkey", "field").ForAwait();

                Assert.False(await del0);
                Assert.True(await del1);
                Assert.False(await del2);
            }
        }

        [Fact]
        public void TestDelMulti() // https://redis.io/commands/hdel
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(3);
                conn.HashSetAsync("TestDelMulti", "key1", "val1");
                conn.HashSetAsync("TestDelMulti", "key2", "val2");
                conn.HashSetAsync("TestDelMulti", "key3", "val3");

                var s1 = conn.HashExistsAsync("TestDelMulti", "key1");
                var s2 = conn.HashExistsAsync("TestDelMulti", "key2");
                var s3 = conn.HashExistsAsync("TestDelMulti", "key3");

                var removed = conn.HashDeleteAsync("TestDelMulti", new RedisValue[] { "key1", "key3" });

                var d1 = conn.HashExistsAsync("TestDelMulti", "key1");
                var d2 = conn.HashExistsAsync("TestDelMulti", "key2");
                var d3 = conn.HashExistsAsync("TestDelMulti", "key3");

                Assert.True(conn.Wait(s1));
                Assert.True(conn.Wait(s2));
                Assert.True(conn.Wait(s3));

                Assert.Equal(2, conn.Wait(removed));

                Assert.False(conn.Wait(d1));
                Assert.True(conn.Wait(d2));
                Assert.False(conn.Wait(d3));

                var removeFinal = conn.HashDeleteAsync("TestDelMulti", new RedisValue[] { "key2" });

                Assert.Equal(0, conn.Wait(conn.HashLengthAsync("TestDelMulti")));
                Assert.Equal(1, conn.Wait(removeFinal));
            }
        }

        [Fact]
        public void TestDelMultiInsideTransaction() // https://redis.io/commands/hdel
        {
            using (var outer = GetUnsecuredConnection())
            {
                var conn = outer.GetDatabase(3).CreateTransaction();
                {
                    conn.HashSetAsync("TestDelMulti", "key1", "val1");
                    conn.HashSetAsync("TestDelMulti", "key2", "val2");
                    conn.HashSetAsync("TestDelMulti", "key3", "val3");

                    var s1 = conn.HashExistsAsync("TestDelMulti", "key1");
                    var s2 = conn.HashExistsAsync("TestDelMulti", "key2");
                    var s3 = conn.HashExistsAsync("TestDelMulti", "key3");

                    var removed = conn.HashDeleteAsync("TestDelMulti", new RedisValue[] { "key1", "key3" });

                    var d1 = conn.HashExistsAsync("TestDelMulti", "key1");
                    var d2 = conn.HashExistsAsync("TestDelMulti", "key2");
                    var d3 = conn.HashExistsAsync("TestDelMulti", "key3");

                    conn.Execute();

                    Assert.True(conn.Wait(s1));
                    Assert.True(conn.Wait(s2));
                    Assert.True(conn.Wait(s3));

                    Assert.Equal(2, conn.Wait(removed));

                    Assert.False(conn.Wait(d1));
                    Assert.True(conn.Wait(d2));
                    Assert.False(conn.Wait(d3));
                }
            }
        }

        [Fact]
        public async Task TestExists() // https://redis.io/commands/hexists
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();
                var ex0 = conn.HashExistsAsync("hashkey", "field").ForAwait();
                await conn.HashSetAsync("hashkey", "field", "value").ForAwait();
                var ex1 = conn.HashExistsAsync("hashkey", "field").ForAwait();
                await conn.HashDeleteAsync("hashkey", "field").ForAwait();
                var ex2 = conn.HashExistsAsync("hashkey", "field").ForAwait();

                Assert.False(await ex0);
                Assert.True(await ex1);
                Assert.False(await ex0);
            }
        }

        [Fact]
        public async Task TestHashKeys() // https://redis.io/commands/hkeys
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                var keys0 = await conn.HashKeysAsync("hashkey").ForAwait();

                await conn.HashSetAsync("hashkey", "foo", "abc").ForAwait();
                await conn.HashSetAsync("hashkey", "bar", "def").ForAwait();

                var keys1 = conn.HashKeysAsync("hashkey");

                Assert.Empty(keys0);

                var arr = await keys1;
                Assert.Equal(2, arr.Length);
                Assert.Equal("foo", arr[0]);
                Assert.Equal("bar", arr[1]);
            }
        }

        [Fact]
        public async Task TestHashValues() // https://redis.io/commands/hvals
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                var keys0 = await conn.HashValuesAsync("hashkey").ForAwait();

                await conn.HashSetAsync("hashkey", "foo", "abc").ForAwait();
                await conn.HashSetAsync("hashkey", "bar", "def").ForAwait();

                var keys1 = conn.HashValuesAsync("hashkey").ForAwait();

                Assert.Empty(keys0);

                var arr = await keys1;
                Assert.Equal(2, arr.Length);
                Assert.Equal("abc", Encoding.UTF8.GetString(arr[0]));
                Assert.Equal("def", Encoding.UTF8.GetString(arr[1]));
            }
        }

        [Fact]
        public async Task TestHashLength() // https://redis.io/commands/hlen
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                var len0 = await conn.HashLengthAsync("hashkey").ForAwait();

                await conn.HashSetAsync("hashkey", "foo", "abc").ForAwait();
                await conn.HashSetAsync("hashkey", "bar", "def").ForAwait();

                var len1 = await conn.HashLengthAsync("hashkey").ForAwait();

                Assert.Equal(0, len0);
                Assert.Equal(2, len1);
            }
        }

        [Fact]
        public async Task TestGetMulti() // https://redis.io/commands/hmget
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                RedisValue[] fields = { "foo", "bar", "blop" };
                var arr0 = await conn.HashGetAsync("hashkey", fields).ForAwait();

                await conn.HashSetAsync("hashkey", "foo", "abc").ForAwait();
                await conn.HashSetAsync("hashkey", "bar", "def").ForAwait();

                var arr1 = await conn.HashGetAsync("hashkey", fields).ForAwait();
                var arr2 = await conn.HashGetAsync("hashkey", fields).ForAwait();

                Assert.Equal(3, arr0.Length);
                Assert.Null((string)arr0[0]);
                Assert.Null((string)arr0[1]);
                Assert.Null((string)arr0[2]);

                Assert.Equal(3, arr1.Length);
                Assert.Equal("abc", arr1[0]);
                Assert.Equal("def", arr1[1]);
                Assert.Null((string)arr1[2]);

                Assert.Equal(3, arr2.Length);
                Assert.Equal("abc", arr2[0]);
                Assert.Equal("def", arr2[1]);
                Assert.Null((string)arr2[2]);
            }
        }

        [Fact]
        public async Task TestGetPairs() // https://redis.io/commands/hgetall
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                var result0 = await conn.HashGetAllAsync("hashkey").ForAwait();

                await conn.HashSetAsync("hashkey", "foo", "abc").ForAwait();
                await conn.HashSetAsync("hashkey", "bar", "def").ForAwait();

                var result1 = await conn.HashGetAllAsync("hashkey").ForAwait();

                Assert.Empty(result0);
                var result = result1.ToStringDictionary();
                Assert.Equal(2, result.Count);
                Assert.Equal("abc", result["foo"]);
                Assert.Equal("def", result["bar"]);
            }
        }

        [Fact]
        public async Task TestSetPairs() // https://redis.io/commands/hmset
        {
            using (var muxer = GetUnsecuredConnection())
            {
                var conn = muxer.GetDatabase(9);
                await conn.KeyDeleteAsync("hashkey").ForAwait();

                var result0 = await conn.HashGetAllAsync("hashkey").ForAwait();

                var data = new HashEntry[] {
                    new HashEntry("foo", Encoding.UTF8.GetBytes("abc")),
                    new HashEntry("bar", Encoding.UTF8.GetBytes("def"))
                };
                await conn.HashSetAsync("hashkey", data).ForAwait();

                var result1 = await conn.HashGetAllAsync("hashkey").ForAwait();

                Assert.Empty(result0);
                var result = result1.ToStringDictionary();
                Assert.Equal(2, result.Count);
                Assert.Equal("abc", result["foo"]);
                Assert.Equal("def", result["bar"]);
            }
        }
    }
}
