using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

namespace StackExchange.Redis.Tests
{
    public class Transactions : TestBase
    {
        public Transactions(ITestOutputHelper output) : base(output) { }

        [Fact]
        public void BasicEmptyTran()
        {
            using (var muxer = Create())
            {
                RedisKey key = Me();
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));

                var tran = db.CreateTransaction();

                var result = tran.Execute();
                Assert.True(result);
            }
        }

        [Fact]
        public void NestedTransactionThrows()
        {
            using (var muxer = Create())
            {
                var db = muxer.GetDatabase();
                object asyncState = new object();
                var tran = db.CreateTransaction();
                var redisTransaction = Assert.IsType<RedisTransaction>(tran);
                Assert.Throws<NotSupportedException>(() => redisTransaction.CreateTransaction(null));
            }
        }

        [Theory]
        [InlineData(false, false, true)]
        [InlineData(false, true, false)]
        [InlineData(true, false, false)]
        [InlineData(true, true, true)]
        public async Task BasicTranWithExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult)
        {
            using (var muxer = Create(disabledCommands: new[] { "info", "config" }))
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);
                if (keyExists) db.StringSet(key2, "any value", flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(keyExists, db.KeyExists(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(demandKeyExists ? Condition.KeyExists(key2) : Condition.KeyNotExists(key2));
                var incr = tran.StringIncrementAsync(key);
                var exec = tran.ExecuteAsync();
                var get = db.StringGet(key);

                Assert.Equal(expectTranResult, await exec);
                if (demandKeyExists == keyExists)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await incr); // eq: incr                    
                    Assert.Equal(1, (long)get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
                    Assert.Equal(0, (long)get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData("same", "same", true, true)]
        [InlineData("x", "y", true, false)]
        [InlineData("x", null, true, false)]
        [InlineData(null, "y", true, false)]
        [InlineData(null, null, true, true)]

        [InlineData("same", "same", false, false)]
        [InlineData("x", "y", false, true)]
        [InlineData("x", null, false, true)]
        [InlineData(null, "y", false, true)]
        [InlineData(null, null, false, false)]
        public async Task BasicTranWithEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult)
        {
            using (var muxer = Create())
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(value, (string)db.StringGet(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(expectEqual ? Condition.StringEqual(key2, expected) : Condition.StringNotEqual(key2, expected));
                var incr = tran.StringIncrementAsync(key);
                var exec = tran.ExecuteAsync();
                var get = db.StringGet(key);

                Assert.Equal(expectTranResult, await exec);
                if (expectEqual == (value == expected))
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await incr); // eq: incr
                    Assert.Equal(1, (long)get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
                    Assert.Equal(0, (long)get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData(false, false, true)]
        [InlineData(false, true, false)]
        [InlineData(true, false, false)]
        [InlineData(true, true, true)]
        public async Task BasicTranWithHashExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult)
        {
            using (var muxer = Create(disabledCommands: new[] { "info", "config" }))
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);
                RedisValue hashField = "field";
                if (keyExists) db.HashSet(key2, hashField, "any value", flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(keyExists, db.HashExists(key2, hashField));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(demandKeyExists ? Condition.HashExists(key2, hashField) : Condition.HashNotExists(key2, hashField));
                var incr = tran.StringIncrementAsync(key);
                var exec = tran.ExecuteAsync();
                var get = db.StringGet(key);

                Assert.Equal(expectTranResult, await exec);
                if (demandKeyExists == keyExists)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await incr); // eq: incr
                    Assert.Equal(1, (long)get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
                    Assert.Equal(0, (long)get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData("same", "same", true, true)]
        [InlineData("x", "y", true, false)]
        [InlineData("x", null, true, false)]
        [InlineData(null, "y", true, false)]
        [InlineData(null, null, true, true)]

        [InlineData("same", "same", false, false)]
        [InlineData("x", "y", false, true)]
        [InlineData("x", null, false, true)]
        [InlineData(null, "y", false, true)]
        [InlineData(null, null, false, false)]
        public async Task BasicTranWithHashEqualsCondition(string expected, string value, bool expectEqual, bool expectedTranResult)
        {
            using (var muxer = Create())
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                RedisValue hashField = "field";
                if (value != null) db.HashSet(key2, hashField, value, flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(value, (string)db.HashGet(key2, hashField));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(expectEqual ? Condition.HashEqual(key2, hashField, expected) : Condition.HashNotEqual(key2, hashField, expected));
                var incr = tran.StringIncrementAsync(key);
                var exec = tran.ExecuteAsync();
                var get = db.StringGet(key);

                Assert.Equal(expectedTranResult, await exec);
                if (expectEqual == (value == expected))
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await incr); // eq: incr
                    Assert.Equal(1, (long)get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
                    Assert.Equal(0, (long)get); // neq: get
                }
            }
        }

        private static TaskStatus SafeStatus(Task task)
        {
            if (task.Status == TaskStatus.WaitingForActivation)
            {
                try
                {
                    if (!task.Wait(1000)) throw new TimeoutException("timeout waiting for task to complete");
                }
                catch (AggregateException ex)
                when (ex.InnerException is TaskCanceledException
                    || (ex.InnerExceptions.Count == 1 && ex.InnerException is TaskCanceledException))
                {
                    return TaskStatus.Canceled;
                }
                catch (TaskCanceledException)
                {
                    return TaskStatus.Canceled;
                }
            }
            return task.Status;
        }

        [Theory]
        [InlineData(false, false, true)]
        [InlineData(false, true, false)]
        [InlineData(true, false, false)]
        [InlineData(true, true, true)]
        public async Task BasicTranWithListExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult)
        {
            using (var muxer = Create(disabledCommands: new[] { "info", "config" }))
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);
                if (keyExists) db.ListRightPush(key2, "any value", flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(keyExists, db.KeyExists(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(demandKeyExists ? Condition.ListIndexExists(key2, 0) : Condition.ListIndexNotExists(key2, 0));
                var push = tran.ListRightPushAsync(key, "any value");
                var exec = tran.ExecuteAsync();
                var get = db.ListGetByIndex(key, 0);

                Assert.Equal(expectTranResult, await exec);
                if (demandKeyExists == keyExists)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await push); // eq: push
                    Assert.Equal("any value", (string)get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push
                    Assert.Null((string)get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData("same", "same", true, true)]
        [InlineData("x", "y", true, false)]
        [InlineData("x", null, true, false)]
        [InlineData(null, "y", true, false)]
        [InlineData(null, null, true, true)]

        [InlineData("same", "same", false, false)]
        [InlineData("x", "y", false, true)]
        [InlineData("x", null, false, true)]
        [InlineData(null, "y", false, true)]
        [InlineData(null, null, false, false)]
        public async Task BasicTranWithListEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult)
        {
            using (var muxer = Create())
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                if (value != null) db.ListRightPush(key2, value, flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(value, (string)db.ListGetByIndex(key2, 0));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(expectEqual ? Condition.ListIndexEqual(key2, 0, expected) : Condition.ListIndexNotEqual(key2, 0, expected));
                var push = tran.ListRightPushAsync(key, "any value");
                var exec = tran.ExecuteAsync();
                var get = db.ListGetByIndex(key, 0);

                Assert.Equal(expectTranResult, await exec);
                if (expectEqual == (value == expected))
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await push); // eq: push
                    Assert.Equal("any value", get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push
                    Assert.Null((string)get); // neq: get
                }
            }
        }

        public enum ComparisonType
        {
            Equal,
            LessThan,
            GreaterThan
        }

        [Theory]
        [InlineData("five", ComparisonType.Equal, 5L, false)]
        [InlineData("four", ComparisonType.Equal, 4L, true)]
        [InlineData("three", ComparisonType.Equal, 3L, false)]
        [InlineData("", ComparisonType.Equal, 2L, false)]
        [InlineData("", ComparisonType.Equal, 0L, true)]
        [InlineData(null, ComparisonType.Equal, 1L, false)]
        [InlineData(null, ComparisonType.Equal, 0L, true)]

        [InlineData("five", ComparisonType.LessThan, 5L, true)]
        [InlineData("four", ComparisonType.LessThan, 4L, false)]
        [InlineData("three", ComparisonType.LessThan, 3L, false)]
        [InlineData("", ComparisonType.LessThan, 2L, true)]
        [InlineData("", ComparisonType.LessThan, 0L, false)]
        [InlineData(null, ComparisonType.LessThan, 1L, true)]
        [InlineData(null, ComparisonType.LessThan, 0L, false)]

        [InlineData("five", ComparisonType.GreaterThan, 5L, false)]
        [InlineData("four", ComparisonType.GreaterThan, 4L, false)]
        [InlineData("three", ComparisonType.GreaterThan, 3L, true)]
        [InlineData("", ComparisonType.GreaterThan, 2L, false)]
        [InlineData("", ComparisonType.GreaterThan, 0L, false)]
        [InlineData(null, ComparisonType.GreaterThan, 1L, false)]
        [InlineData(null, ComparisonType.GreaterThan, 0L, false)]
        public async Task BasicTranWithStringLengthCondition(string value, ComparisonType type, long length, bool expectTranResult)
        {
            using (var muxer = Create())
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                var expectSuccess = false;
                Condition condition = null;
                var valueLength = value?.Length ?? 0;
                switch (type)
                {
                    case ComparisonType.Equal:
                        expectSuccess = valueLength == length;
                        condition = Condition.StringLengthEqual(key2, length);
                        Assert.Contains("String length == " + length, condition.ToString());
                        break;
                    case ComparisonType.GreaterThan:
                        expectSuccess = valueLength > length;
                        condition = Condition.StringLengthGreaterThan(key2, length);
                        Assert.Contains("String length > " + length, condition.ToString());
                        break;
                    case ComparisonType.LessThan:
                        expectSuccess = valueLength < length;
                        condition = Condition.StringLengthLessThan(key2, length);
                        Assert.Contains("String length < " + length, condition.ToString());
                        break;
                }

                if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(value, db.StringGet(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(condition);
                var push = tran.StringSetAsync(key, "any value");
                var exec = tran.ExecuteAsync();
                var get = db.StringLength(key);

                Assert.Equal(expectTranResult, await exec);

                if (expectSuccess)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.True(await push); // eq: push
                    Assert.Equal("any value".Length, get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push
                    Assert.Equal(0, get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData("five", ComparisonType.Equal, 5L, false)]
        [InlineData("four", ComparisonType.Equal, 4L, true)]
        [InlineData("three", ComparisonType.Equal, 3L, false)]
        [InlineData("", ComparisonType.Equal, 2L, false)]
        [InlineData("", ComparisonType.Equal, 0L, true)]

        [InlineData("five", ComparisonType.LessThan, 5L, true)]
        [InlineData("four", ComparisonType.LessThan, 4L, false)]
        [InlineData("three", ComparisonType.LessThan, 3L, false)]
        [InlineData("", ComparisonType.LessThan, 2L, true)]
        [InlineData("", ComparisonType.LessThan, 0L, false)]

        [InlineData("five", ComparisonType.GreaterThan, 5L, false)]
        [InlineData("four", ComparisonType.GreaterThan, 4L, false)]
        [InlineData("three", ComparisonType.GreaterThan, 3L, true)]
        [InlineData("", ComparisonType.GreaterThan, 2L, false)]
        [InlineData("", ComparisonType.GreaterThan, 0L, false)]
        public async Task BasicTranWithHashLengthCondition(string value, ComparisonType type, long length, bool expectTranResult)
        {
            using (var muxer = Create())
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                var expectSuccess = false;
                Condition condition = null;
                var valueLength = value?.Length ?? 0;
                switch (type)
                {
                    case ComparisonType.Equal:
                        expectSuccess = valueLength == length;
                        condition = Condition.HashLengthEqual(key2, length);
                        break;
                    case ComparisonType.GreaterThan:
                        expectSuccess = valueLength > length;
                        condition = Condition.HashLengthGreaterThan(key2, length);
                        break;
                    case ComparisonType.LessThan:
                        expectSuccess = valueLength < length;
                        condition = Condition.HashLengthLessThan(key2, length);
                        break;
                }

                for (var i = 0; i < valueLength; i++)
                {
                    db.HashSet(key2, i, value[i].ToString(), flags: CommandFlags.FireAndForget);
                }
                Assert.False(db.KeyExists(key));
                Assert.Equal(valueLength, db.HashLength(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(condition);
                var push = tran.StringSetAsync(key, "any value");
                var exec = tran.ExecuteAsync();
                var get = db.StringLength(key);

                Assert.Equal(expectTranResult, await exec);

                if (expectSuccess)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.True(await push); // eq: push
                    Assert.Equal("any value".Length, get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push
                    Assert.Equal(0, get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData("five", ComparisonType.Equal, 5L, false)]
        [InlineData("four", ComparisonType.Equal, 4L, true)]
        [InlineData("three", ComparisonType.Equal, 3L, false)]
        [InlineData("", ComparisonType.Equal, 2L, false)]
        [InlineData("", ComparisonType.Equal, 0L, true)]

        [InlineData("five", ComparisonType.LessThan, 5L, true)]
        [InlineData("four", ComparisonType.LessThan, 4L, false)]
        [InlineData("three", ComparisonType.LessThan, 3L, false)]
        [InlineData("", ComparisonType.LessThan, 2L, true)]
        [InlineData("", ComparisonType.LessThan, 0L, false)]

        [InlineData("five", ComparisonType.GreaterThan, 5L, false)]
        [InlineData("four", ComparisonType.GreaterThan, 4L, false)]
        [InlineData("three", ComparisonType.GreaterThan, 3L, true)]
        [InlineData("", ComparisonType.GreaterThan, 2L, false)]
        [InlineData("", ComparisonType.GreaterThan, 0L, false)]
        public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult)
        {
            using (var muxer = Create())
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                var expectSuccess = false;
                Condition condition = null;
                var valueLength = value?.Length ?? 0;
                switch (type)
                {
                    case ComparisonType.Equal:
                        expectSuccess = valueLength == length;
                        condition = Condition.SetLengthEqual(key2, length);
                        break;
                    case ComparisonType.GreaterThan:
                        expectSuccess = valueLength > length;
                        condition = Condition.SetLengthGreaterThan(key2, length);
                        break;
                    case ComparisonType.LessThan:
                        expectSuccess = valueLength < length;
                        condition = Condition.SetLengthLessThan(key2, length);
                        break;
                }

                for (var i = 0; i < valueLength; i++)
                {
                    db.SetAdd(key2, i, flags: CommandFlags.FireAndForget);
                }
                Assert.False(db.KeyExists(key));
                Assert.Equal(valueLength, db.SetLength(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(condition);
                var push = tran.StringSetAsync(key, "any value");
                var exec = tran.ExecuteAsync();
                var get = db.StringLength(key);

                Assert.Equal(expectTranResult, await exec);

                if (expectSuccess)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.True(await push); // eq: push
                    Assert.Equal("any value".Length, get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push
                    Assert.Equal(0, get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData(false, false, true)]
        [InlineData(false, true, false)]
        [InlineData(true, false, false)]
        [InlineData(true, true, true)]
        public async Task BasicTranWithSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult)
        {
            using (var muxer = Create(disabledCommands: new[] { "info", "config" }))
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);
                RedisValue member = "value";
                if (keyExists) db.SetAdd(key2, member, flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(keyExists, db.SetContains(key2, member));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(demandKeyExists ? Condition.SetContains(key2, member) : Condition.SetNotContains(key2, member));
                var incr = tran.StringIncrementAsync(key);
                var exec = tran.ExecuteAsync();
                var get = db.StringGet(key);

                Assert.Equal(expectTranResult, await exec);
                if (demandKeyExists == keyExists)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await incr); // eq: incr
                    Assert.Equal(1, (long)get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
                    Assert.Equal(0, (long)get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData("five", ComparisonType.Equal, 5L, false)]
        [InlineData("four", ComparisonType.Equal, 4L, true)]
        [InlineData("three", ComparisonType.Equal, 3L, false)]
        [InlineData("", ComparisonType.Equal, 2L, false)]
        [InlineData("", ComparisonType.Equal, 0L, true)]

        [InlineData("five", ComparisonType.LessThan, 5L, true)]
        [InlineData("four", ComparisonType.LessThan, 4L, false)]
        [InlineData("three", ComparisonType.LessThan, 3L, false)]
        [InlineData("", ComparisonType.LessThan, 2L, true)]
        [InlineData("", ComparisonType.LessThan, 0L, false)]

        [InlineData("five", ComparisonType.GreaterThan, 5L, false)]
        [InlineData("four", ComparisonType.GreaterThan, 4L, false)]
        [InlineData("three", ComparisonType.GreaterThan, 3L, true)]
        [InlineData("", ComparisonType.GreaterThan, 2L, false)]
        [InlineData("", ComparisonType.GreaterThan, 0L, false)]
        public async Task BasicTranWithSortedSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult)
        {
            using (var muxer = Create())
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                var expectSuccess = false;
                Condition condition = null;
                var valueLength = value?.Length ?? 0;
                switch (type)
                {
                    case ComparisonType.Equal:
                        expectSuccess = valueLength == length;
                        condition = Condition.SortedSetLengthEqual(key2, length);
                        break;
                    case ComparisonType.GreaterThan:
                        expectSuccess = valueLength > length;
                        condition = Condition.SortedSetLengthGreaterThan(key2, length);
                        break;
                    case ComparisonType.LessThan:
                        expectSuccess = valueLength < length;
                        condition = Condition.SortedSetLengthLessThan(key2, length);
                        break;
                }

                for (var i = 0; i < valueLength; i++)
                {
                    db.SortedSetAdd(key2, i, i, flags: CommandFlags.FireAndForget);
                }
                Assert.False(db.KeyExists(key));
                Assert.Equal(valueLength, db.SortedSetLength(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(condition);
                var push = tran.StringSetAsync(key, "any value");
                var exec = tran.ExecuteAsync();
                var get = db.StringLength(key);

                Assert.Equal(expectTranResult, await exec);

                if (expectSuccess)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.True(await push); // eq: push
                    Assert.Equal("any value".Length, get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push
                    Assert.Equal(0, get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData(false, false, true)]
        [InlineData(false, true, false)]
        [InlineData(true, false, false)]
        [InlineData(true, true, true)]
        public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult)
        {
            using (var muxer = Create(disabledCommands: new[] { "info", "config" }))
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);
                RedisValue member = "value";
                if (keyExists) db.SortedSetAdd(key2, member, 0.0, flags: CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));
                Assert.Equal(keyExists, db.SortedSetScore(key2, member).HasValue);

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetContains(key2, member) : Condition.SortedSetNotContains(key2, member));
                var incr = tran.StringIncrementAsync(key);
                var exec = tran.ExecuteAsync();
                var get = db.StringGet(key);

                Assert.Equal(expectTranResult, await exec);
                if (demandKeyExists == keyExists)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.Equal(1, await incr); // eq: incr
                    Assert.Equal(1, (long)get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
                    Assert.Equal(0, (long)get); // neq: get
                }
            }
        }

        [Theory]
        [InlineData("five", ComparisonType.Equal, 5L, false)]
        [InlineData("four", ComparisonType.Equal, 4L, true)]
        [InlineData("three", ComparisonType.Equal, 3L, false)]
        [InlineData("", ComparisonType.Equal, 2L, false)]
        [InlineData("", ComparisonType.Equal, 0L, true)]

        [InlineData("five", ComparisonType.LessThan, 5L, true)]
        [InlineData("four", ComparisonType.LessThan, 4L, false)]
        [InlineData("three", ComparisonType.LessThan, 3L, false)]
        [InlineData("", ComparisonType.LessThan, 2L, true)]
        [InlineData("", ComparisonType.LessThan, 0L, false)]

        [InlineData("five", ComparisonType.GreaterThan, 5L, false)]
        [InlineData("four", ComparisonType.GreaterThan, 4L, false)]
        [InlineData("three", ComparisonType.GreaterThan, 3L, true)]
        [InlineData("", ComparisonType.GreaterThan, 2L, false)]
        [InlineData("", ComparisonType.GreaterThan, 0L, false)]
        public async Task BasicTranWithListLengthCondition(string value, ComparisonType type, long length, bool expectTranResult)
        {
            using (var muxer = Create(log: TextWriter.Null))
            {
                RedisKey key = Me(), key2 = Me() + "2";
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                db.KeyDelete(key2, CommandFlags.FireAndForget);

                var expectSuccess = false;
                Condition condition = null;
                var valueLength = value?.Length ?? 0;
                switch (type)
                {
                    case ComparisonType.Equal:
                        expectSuccess = valueLength == length;
                        condition = Condition.ListLengthEqual(key2, length);
                        break;
                    case ComparisonType.GreaterThan:
                        expectSuccess = valueLength > length;
                        condition = Condition.ListLengthGreaterThan(key2, length);
                        break;
                    case ComparisonType.LessThan:
                        expectSuccess = valueLength < length;
                        condition = Condition.ListLengthLessThan(key2, length);
                        break;
                }

                for (var i = 0; i < valueLength; i++)
                {
                    db.ListRightPush(key2, i, flags: CommandFlags.FireAndForget);
                }
                Assert.False(db.KeyExists(key));
                Assert.Equal(valueLength, db.ListLength(key2));

                var tran = db.CreateTransaction();
                var cond = tran.AddCondition(condition);
                var push = tran.StringSetAsync(key, "any value");
                var exec = tran.ExecuteAsync();
                var get = db.StringLength(key);

                Assert.Equal(expectTranResult, await exec);

                if (expectSuccess)
                {
                    Assert.True(await exec, "eq: exec");
                    Assert.True(cond.WasSatisfied, "eq: was satisfied");
                    Assert.True(await push); // eq: push
                    Assert.Equal("any value".Length, get); // eq: get
                }
                else
                {
                    Assert.False(await exec, "neq: exec");
                    Assert.False(cond.WasSatisfied, "neq: was satisfied");
                    Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push
                    Assert.Equal(0, get); // neq: get
                }
            }
        }

        [Fact]
        public async Task BasicTran()
        {
            using (var muxer = Create())
            {
                RedisKey key = Me();
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));

                var tran = db.CreateTransaction();
                var a = tran.StringIncrementAsync(key, 10);
                var b = tran.StringIncrementAsync(key, 5);
                var c = tran.StringGetAsync(key);
                var d = tran.KeyExistsAsync(key);
                var e = tran.KeyDeleteAsync(key);
                var f = tran.KeyExistsAsync(key);
                Assert.False(a.IsCompleted);
                Assert.False(b.IsCompleted);
                Assert.False(c.IsCompleted);
                Assert.False(d.IsCompleted);
                Assert.False(e.IsCompleted);
                Assert.False(f.IsCompleted);
                var result = await tran.ExecuteAsync().ForAwait();
                Assert.True(result, "result");
                await Task.WhenAll(a, b, c, d, e, f).ForAwait();
                Assert.True(a.IsCompleted, "a");
                Assert.True(b.IsCompleted, "b");
                Assert.True(c.IsCompleted, "c");
                Assert.True(d.IsCompleted, "d");
                Assert.True(e.IsCompleted, "e");
                Assert.True(f.IsCompleted, "f");

                var g = db.KeyExists(key);

                Assert.Equal(10, await a.ForAwait());
                Assert.Equal(15, await b.ForAwait());
                Assert.Equal(15, (long)await c.ForAwait());
                Assert.True(await d.ForAwait());
                Assert.True(await e.ForAwait());
                Assert.False(await f.ForAwait());
                Assert.False(g);
            }
        }

        [Fact]
        public async Task CombineFireAndForgetAndRegularAsyncInTransaction()
        {
            using (var muxer = Create())
            {
                RedisKey key = Me();
                var db = muxer.GetDatabase();
                db.KeyDelete(key, CommandFlags.FireAndForget);
                Assert.False(db.KeyExists(key));

                var tran = db.CreateTransaction("state");
                var a = tran.StringIncrementAsync(key, 5);
                var b = tran.StringIncrementAsync(key, 10, CommandFlags.FireAndForget);
                var c = tran.StringIncrementAsync(key, 15);
                Assert.True(tran.Execute());
                var count = (long)db.StringGet(key);

                Assert.Equal(5, await a);
                Assert.Equal("state", a.AsyncState);
                Assert.Equal(0, await b);
                Assert.Null(b.AsyncState);
                Assert.Equal(30, await c);
                Assert.Equal("state", a.AsyncState);
                Assert.Equal(30, count);
            }
        }

        [Fact]
        public async Task ParallelTransactionsWithConditions()
        {
            const int Muxers = 4, Workers = 20, PerThread = 250;

            var muxers = new ConnectionMultiplexer[Muxers];
            try
            {
                for (int i = 0; i < Muxers; i++)
                    muxers[i] = Create(log: TextWriter.Null);

                RedisKey hits = Me(), trigger = Me() + "3";
                int expectedSuccess = 0;

                await muxers[0].GetDatabase().KeyDeleteAsync(new[] { hits, trigger });

                Task[] tasks = new Task[Workers];
                for (int i = 0; i < tasks.Length; i++)
                {
                    var scopedDb = muxers[i % Muxers].GetDatabase();
                    var rand = new Random(i);
                    tasks[i] = Task.Run(async () =>
                    {
                        for (int j = 0; j < PerThread; j++)
                        {
                            var oldVal = await scopedDb.StringGetAsync(trigger);
                            var tran = scopedDb.CreateTransaction();
                            tran.AddCondition(Condition.StringEqual(trigger, oldVal));
                            var x = tran.StringIncrementAsync(trigger);
                            var y = tran.StringIncrementAsync(hits);
                            if (await tran.ExecuteAsync())
                            {
                                Interlocked.Increment(ref expectedSuccess);
                                await x;
                                await y;
                            }
                            else
                            {
                                await Assert.ThrowsAsync<TaskCanceledException>(() => x);
                                await Assert.ThrowsAsync<TaskCanceledException>(() => y);
                            }
                        }
                    });
                }
                for (int i = tasks.Length - 1; i >= 0; i--)
                {
                    await tasks[i];
                }
                var actual = (int)await muxers[0].GetDatabase().StringGetAsync(hits);
                Assert.Equal(expectedSuccess, actual);
                Writer.WriteLine($"success: {actual} out of {Workers * PerThread} attempts");
            }
            finally
            {
                for (int i = 0; i < muxers.Length; i++)
                {
                    try { muxers[i]?.Dispose(); } catch { }
                }
            }
        }

        [Fact]
        public async Task WatchAbort_StringEqual()
        {
            using (var vic = Create(log: TextWriter.Null))
            using (var perp = Create(log: TextWriter.Null))
            {
                var key = Me();
                var db = vic.GetDatabase();

                // expect foo, change to bar at the last minute
                vic.PreTransactionExec += cmd =>
                {
                    Writer.WriteLine($"'{cmd}' detected; changing it...");
                    perp.GetDatabase().StringSet(key, "bar");
                };
                db.KeyDelete(key);
                db.StringSet(key, "foo");
                var tran = db.CreateTransaction();
                tran.AddCondition(Condition.StringEqual(key, "foo"));
                var pong = tran.PingAsync();
                Assert.False(await tran.ExecuteAsync());
                await Assert.ThrowsAsync<TaskCanceledException>(() => pong);
            }
        }

        [Fact]
        public async Task WatchAbort_HashLengthEqual()
        {
            using (var vic = Create(log: TextWriter.Null))
            using (var perp = Create(log: TextWriter.Null))
            {
                var key = Me();
                var db = vic.GetDatabase();

                // expect foo, change to bar at the last minute
                vic.PreTransactionExec += cmd =>
                {
                    Writer.WriteLine($"'{cmd}' detected; changing it...");
                    perp.GetDatabase().HashSet(key, "bar", "def");
                };
                db.KeyDelete(key);
                db.HashSet(key, "foo", "abc");
                var tran = db.CreateTransaction();
                tran.AddCondition(Condition.HashLengthEqual(key, 1));
                var pong = tran.PingAsync();
                Assert.False(await tran.ExecuteAsync());
                await Assert.ThrowsAsync<TaskCanceledException>(() => pong);
            }
        }
    }
}
