using NSubstitute;
using NUnit.Framework;
using Plugin.BLE.Abstractions.Contracts;
using System;
using System.Threading.Tasks;
using TINK.Services.BluetoothLock.Tdo;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.BLE;
using DeviceState = Plugin.BLE.Abstractions.DeviceState;
using Plugin.BLE.Abstractions.EventArgs;
using System.Threading;

namespace TestLockItBLE
{
    public class Tests
    {
        [Test]
        public void TestOpen()
        {
            var device = Substitute.For<IDevice>();
            var adapter = Substitute.For<IAdapter>();
            var cipher = Substitute.For<TINK.Model.Device.ICipher>();
            var auth = Substitute.For<ICharacteristic>();
            var controlService = Substitute.For<IService>();
            var controlCharacteristic = Substitute.For<ICharacteristic>();
            var stateCharacteristic = Substitute.For<ICharacteristic>();
            var activateCharacteristic = Substitute.For<ICharacteristic>();

            var authTdo = new LockInfoAuthTdo.Builder
            {
                Id = 12,
                K_seed = new byte[] { (byte)'p', (byte)'a', (byte)'w', (byte)'m', (byte)'X', (byte)'8', (byte)'T', (byte)'X', (byte)'Q', (byte)'Z', (byte)'d', (byte)'l', (byte)'k', (byte)'3', (byte)'e', (byte)'V', },
                K_u = new byte[16]
            }.Build();

            // Calls related to Authenticate functionality.
            device.State.Returns(DeviceState.Connected);
            device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
            device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
            controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
            cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
            cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);

            device.Name.Returns("ISHAREIT+123");

            // Calls related to open functionality.
            controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
            controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* State read before open action: closed */ }));
            controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateCharacteristic));
            activateCharacteristic.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Open /* State passed as event argument after opening. */});

            // Use factory to create LockIt-object.
            var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
            var lockState = lockIt.OpenAsync();
            controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
            Assert.That(lockState.Result, Is.EqualTo(LockitLockingState.Open));
        }

        [Test]
        public void TestOpen_ThrowsCouldntOpenInconsistentStateExecption()
        {
            var device = Substitute.For<IDevice>();
            var adapter = Substitute.For<IAdapter>();
            var cipher = Substitute.For<TINK.Model.Device.ICipher>();
            var auth = Substitute.For<ICharacteristic>();
            var controlService = Substitute.For<IService>();
            var controlCharacteristic = Substitute.For<ICharacteristic>();
            var stateCharacteristic = Substitute.For<ICharacteristic>();
            var activateLock = Substitute.For<ICharacteristic>();

            var authTdo = new LockInfoAuthTdo.Builder
            {
                Id = 12,
                K_seed = new byte[] { (byte)'m', (byte)'l', (byte)'Q', (byte)'I', (byte)'S', (byte)'z', (byte)'p', (byte)'H', (byte)'m', (byte)'n', (byte)'V', (byte)'n', (byte)'7', (byte)'f', (byte)'i', (byte)'3' },
                K_u = new byte[16]
            }.Build();

            // Calls related to Authenticate functionality.
            device.State.Returns(DeviceState.Connected);
            device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
            device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
            controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
            cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
            cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);

            device.Name.Returns("ISHAREIT+123");

            // Calls related to open functionality.
            controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
            controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* State read before open action: closed */ }));
            controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
            activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Closed /* State passed as event argument after opening. */});

            // Use factory to create LockIt-object.
            var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
            Assert.That(async () => 
                {
                    var lockState = lockIt.OpenAsync();
                    controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
                    await lockState;
                }, 
            Throws.InstanceOf<CouldntOpenInconsistentStateExecption>());
        }

        [Test]
        public void TestOpen_ThrowsCouldntOpenBoldBlockedException()
        {
            var device = Substitute.For<IDevice>();
            var adapter = Substitute.For<IAdapter>();
            var cipher = Substitute.For<TINK.Model.Device.ICipher>();
            var auth = Substitute.For<ICharacteristic>();
            var lockControl = Substitute.For<IService>();
            var controlCharacteristic = Substitute.For<ICharacteristic>();
            var activateLock = Substitute.For<ICharacteristic>();
            var stateCharacteristic = Substitute.For<ICharacteristic>();
            
            var authTdo = new LockInfoAuthTdo.Builder
            {
                Id = 12,
                K_seed = new byte[] { (byte)'i', (byte)'r', (byte)'F', (byte)'h', (byte)'G', (byte)'T', (byte)'z', (byte)'P', (byte)'F', (byte)'Z', (byte)'n', (byte)'z', (byte)'Y', (byte)'B', (byte)'s', (byte)'9' },
                K_u = new byte[16]
            }.Build();

            // Calls related to Authenticate functionality.
            device.State.Returns(DeviceState.Connected);
            device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
            device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
            lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
            cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
            cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);

            device.Name.Returns("ISHAREIT+123");

            // Calls related to open functionality.
            lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
            controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* closed */}), Task.FromResult(new byte[] { 4 /* bold blocked */ }));
            lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
            activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.CouldntOpenBoldBlocked /* State passed as event argument after opening. */});

            var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;

            // Use factory to create LockIt-object.
            Assert.That(async () =>
            {
                var lockState = lockIt.OpenAsync();
                controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
                await lockState;
            },
            Throws.InstanceOf<CouldntOpenBoldIsBlockedException>());
        }

        [Test]
        public void TestClose()
        {
            var device = Substitute.For<IDevice>();
            var adapter = Substitute.For<IAdapter>();
            var cipher = Substitute.For<TINK.Model.Device.ICipher>();
            var auth = Substitute.For<ICharacteristic>();
            var lockControl = Substitute.For<IService>();
            var controlCharacteristic = Substitute.For<ICharacteristic>();
            var activateLock = Substitute.For<ICharacteristic>();
            var stateCharacteristic = Substitute.For<ICharacteristic>();

            var authTdo = new LockInfoAuthTdo.Builder
            {
                Id = 12,
                K_seed = new byte[] { (byte)'I', (byte)'7', (byte)'y', (byte)'B', (byte)'f', (byte)'s', (byte)'v', (byte)'L', (byte)'G', (byte)'L', (byte)'7', (byte)'b', (byte)'7', (byte)'X', (byte)'z', (byte)'t' },
                K_u = new byte[16]
            }.Build();

            // Calls related to Authenticate functionality.
            device.State.Returns(DeviceState.Connected);
            device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
            device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
            lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
            cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
            cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);

            device.Name.Returns("ISHAREIT+123");

            // Calls related to close functionality.
            lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
            controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* open */ }), Task.FromResult(new byte[] { 1 /* closed */}));
            lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
            activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Closed /* State passed as event argument after closing. */});

            // Use factory to create LockIt-object.
            var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;

            var lockState = lockIt.CloseAsync();
            controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
            Assert.That(lockState.Result, Is.EqualTo(LockitLockingState.Closed));
        }

        [Test]
        public void TestClose_ThrowsCouldntCloseInconsistentStateExecption()
        {
            var device = Substitute.For<IDevice>();
            var adapter = Substitute.For<IAdapter>();
            var cipher = Substitute.For<TINK.Model.Device.ICipher>();
            var auth = Substitute.For<ICharacteristic>();
            var lockControl = Substitute.For<IService>();
            var controlCharacteristic = Substitute.For<ICharacteristic>();
            var activateLock = Substitute.For<ICharacteristic>();
            var stateCharacteristic = Substitute.For<ICharacteristic>();

            var authTdo = new LockInfoAuthTdo.Builder
            {
                Id = 12,
                K_seed = new byte[] { (byte)'8', (byte)'q', (byte)'3', (byte)'9', (byte)'i', (byte)'6', (byte)'c', (byte)'g', (byte)'9', (byte)'L', (byte)'V', (byte)'7', (byte)'T', (byte)'G', (byte)'l', (byte)'f' },
                K_u = new byte[16]
            }.Build();

            // Calls related to Authenticate functionality.
            device.State.Returns(DeviceState.Connected);
            device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
            device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
            lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
            cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
            cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);

            device.Name.Returns("ISHAREIT+123");

            // Calls related to close functionality.
            lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
            controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* opened */}));
            lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
            activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Open /* State passed as event argument after closing. */});

            // Use factory to create LockIt-object.
            var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;

            // Use factory to create LockIt-object.
            Assert.That(async () =>
            {
                var lockState = lockIt.CloseAsync();
                controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
                await lockState;
            },
            Throws.InstanceOf<CouldntCloseInconsistentStateExecption>());
        }

        [Test]
        public void TestClose_ThrowsCouldntCloseBoldBlockedException()
        {
            var device = Substitute.For<IDevice>();
            var adapter = Substitute.For<IAdapter>();
            var cipher = Substitute.For<TINK.Model.Device.ICipher>();
            var auth = Substitute.For<ICharacteristic>();
            var lockControl = Substitute.For<IService>();
            var controlCharacteristic = Substitute.For<ICharacteristic>();
            var activateLock = Substitute.For<ICharacteristic>();
            var stateCharacteristic = Substitute.For<ICharacteristic>();

            var authTdo = new LockInfoAuthTdo.Builder
            {
                Id = 12,
                K_seed = new byte[] { (byte)'v', (byte)'f', (byte)'u', (byte)'v', (byte)'j', (byte)'E', (byte)'b', (byte)'x', (byte)'p', (byte)'z', (byte)'a', (byte)'h', (byte)'V', (byte)'5', (byte)'9', (byte)'i' },
                K_u = new byte[16]
            }.Build();

            // Calls related to Authenticate functionality.
            device.State.Returns(DeviceState.Connected);
            device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
            device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
            lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
            cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
            cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
            auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);

            device.Name.Returns("ISHAREIT+123");

            // Calls related to Close functionality.
            lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
            controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* open */}));
            lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
            activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
            stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.CouldntCloseBoldBlocked /* State passed as event argument after opening. */});

            // Use factory to create LockIt-object.
            var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;

            // Use factory to create LockIt-object.
            Assert.That(async () =>
            {
                var lockState = lockIt.CloseAsync();
                controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
                await lockState;
            },
            Throws.InstanceOf<CouldntCloseBoldBlockedException>());
        }
    }
}