package com.welie.blessed;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import static android.bluetooth.BluetoothDevice.BOND_BONDED;
import static android.bluetooth.BluetoothDevice.BOND_BONDING;
import static android.bluetooth.BluetoothDevice.BOND_NONE;
import static android.bluetooth.BluetoothGatt.CONNECTION_PRIORITY_HIGH;
import static android.bluetooth.BluetoothGatt.GATT_FAILURE;
import static android.bluetooth.BluetoothGatt.GATT_SUCCESS;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_INDICATE;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
import static android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING;
import static android.os.Build.VERSION_CODES.M;
import static junit.framework.Assert.assertFalse;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = { M })
public class BluetoothPeripheralTest {
    private BluetoothPeripheral peripheral;
    private Handler handler;
    public static final UUID SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb");

    @Mock
    private BluetoothPeripheral.InternalCallback internalCallback;

    @Mock
    private BluetoothPeripheralCallback peripheralCallback;

    @Mock
    private Context context;

    @Mock
    private BluetoothDevice device;

    @Mock
    private BluetoothGatt gatt;

    @Before
    public void setUp() throws Exception {
        initMocks(this);

        when(device.getAddress()).thenReturn("12:23:34:98:76:54");
        when(device.connectGatt(any(Context.class), anyBoolean(), any(BluetoothGattCallback.class), anyInt())).thenReturn(gatt);
        when(gatt.getDevice()).thenReturn(device);


        peripheral = new BluetoothPeripheral(context, device, internalCallback, peripheralCallback, handler);
    }

    @Test
    public void connectTest() throws Exception {
        peripheral.connect();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        ArgumentCaptor<BluetoothGattCallback> captor = ArgumentCaptor.forClass(BluetoothGattCallback.class);

        verify(device).connectGatt(any(Context.class), anyBoolean(), captor.capture(), anyInt());

        BluetoothGattCallback callback = captor.getValue();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        assertEquals(STATE_CONNECTED,  peripheral.getState());
    }

    @Test
    public void autoconnectTest() throws Exception {
        peripheral.autoConnect();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        ArgumentCaptor<BluetoothGattCallback> captor = ArgumentCaptor.forClass(BluetoothGattCallback.class);
        ArgumentCaptor<Boolean> autoConnectCaptor = ArgumentCaptor.forClass(Boolean.class);

        verify(device).connectGatt(any(Context.class), autoConnectCaptor.capture(), captor.capture(), anyInt());

        boolean autoconnect = autoConnectCaptor.getValue();
        assertTrue(autoconnect);

        BluetoothGattCallback callback = captor.getValue();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        assertEquals(STATE_CONNECTED,  peripheral.getState());
    }

    @Test
    public void cancelConnectionTest() throws Exception {
        peripheral.connect();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        ArgumentCaptor<BluetoothGattCallback> captor = ArgumentCaptor.forClass(BluetoothGattCallback.class);

        verify(device).connectGatt(any(Context.class), anyBoolean(), captor.capture(), anyInt());

        BluetoothGattCallback callback = captor.getValue();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        peripheral.cancelConnection();

        verify(gatt).disconnect();

        assertEquals(STATE_DISCONNECTING,  peripheral.getState());

        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_DISCONNECTED);

        verify(gatt).close();
    }

    @Test
    public void disconnectNullTest() throws Exception {
        peripheral.cancelConnection();

        verify(gatt, never()).disconnect();
    }


    @Test
    public void getAddressTest() throws Exception {
        assertEquals("12:23:34:98:76:54", peripheral.getAddress());
    }


    @Test
    public void setNotifyIndicationTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_INDICATE,0);
        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),0);
        service.addCharacteristic(characteristic);
        characteristic.addDescriptor(descriptor);

        when(gatt.getServices()).thenReturn(Arrays.asList(service));
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        peripheral.setNotify(characteristic, true);
        verify(gatt).setCharacteristicNotification(characteristic, true);
        assertEquals(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE[0], descriptor.getValue()[0]);
        assertEquals(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE[1], descriptor.getValue()[1]);
        verify(gatt).writeDescriptor(descriptor);

        callback.onDescriptorWrite(gatt, descriptor, 0);
        verify(peripheralCallback).onNotificationStateUpdate(peripheral, characteristic, 0);
    }

    @Test
    public void setNotifyNotificationTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_NOTIFY,0);
        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),0);
        service.addCharacteristic(characteristic);
        characteristic.addDescriptor(descriptor);

        when(gatt.getServices()).thenReturn(Arrays.asList(service));

        peripheral.setNotify(characteristic, true);
        verify(gatt).setCharacteristicNotification(characteristic, true);
        assertEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE[0], descriptor.getValue()[0]);
        assertEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE[1], descriptor.getValue()[1]);
        verify(gatt).writeDescriptor(descriptor);

        callback.onDescriptorWrite(gatt, descriptor, 0);
        verify(peripheralCallback).onNotificationStateUpdate(peripheral, characteristic, 0);
    }

    @Test
    public void setNotifyDisableTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_NOTIFY,0);
        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),0);
        service.addCharacteristic(characteristic);
        characteristic.addDescriptor(descriptor);

        when(gatt.getServices()).thenReturn(Arrays.asList(service));

        peripheral.setNotify(characteristic, false);
        verify(gatt).setCharacteristicNotification(characteristic, false);
        assertEquals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE[0], descriptor.getValue()[0]);
        assertEquals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE[1], descriptor.getValue()[1]);
        verify(gatt).writeDescriptor(descriptor);

        callback.onDescriptorWrite(gatt, descriptor, 0);

        verify(peripheralCallback).onNotificationStateUpdate(peripheral, characteristic, 0);
    }

    @Test
    public void readCharacteristicTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), PROPERTY_READ,0);
        characteristic.setValue(new byte[]{0x00});
        when(gatt.readCharacteristic(characteristic)).thenReturn(true);

        peripheral.readCharacteristic(characteristic);

        verify(gatt).readCharacteristic(any(BluetoothGattCharacteristic.class));

        byte[] originalByteArray = new byte[]{0x01};
        characteristic.setValue(originalByteArray);
        callback.onCharacteristicRead(gatt, characteristic, 0);

        ArgumentCaptor<BluetoothPeripheral> captorPeripheral = ArgumentCaptor.forClass(BluetoothPeripheral.class);
        ArgumentCaptor<byte[]> captorValue = ArgumentCaptor.forClass(byte[].class);
        ArgumentCaptor<BluetoothGattCharacteristic> captorCharacteristic = ArgumentCaptor.forClass(BluetoothGattCharacteristic.class);
        ArgumentCaptor<Integer> captorStatus = ArgumentCaptor.forClass(Integer.class);
        verify(peripheralCallback).onCharacteristicUpdate(captorPeripheral.capture(), captorValue.capture(), captorCharacteristic.capture(), captorStatus.capture());

        byte[] value = captorValue.getValue();
        assertEquals(0x01, value[0]);
        assertNotEquals(value, originalByteArray);   // Check if the byte array has been copier
        assertEquals(peripheral, captorPeripheral.getValue());
        assertEquals(characteristic, captorCharacteristic.getValue());
        assertEquals(GATT_SUCCESS, (int) captorStatus.getValue() );
    }

    @Test
    public void readCharacteristicNotConnectedTest() throws Exception {
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), PROPERTY_READ,0);

        when(gatt.readCharacteristic(characteristic)).thenReturn(true);

        peripheral.readCharacteristic(characteristic);

        verify(gatt, never()).readCharacteristic(any(BluetoothGattCharacteristic.class));
    }

    @Test
    public void readCharacteristicNoReadPropertyTest() throws Exception {
        peripheral.connect();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), 0,0);

        peripheral.readCharacteristic(characteristic);

        verify(gatt, never()).readCharacteristic(any(BluetoothGattCharacteristic.class));
    }

    @Test
    public void writeCharacteristicTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), PROPERTY_WRITE,0);

        when(gatt.writeCharacteristic(characteristic)).thenReturn(true);

        peripheral.writeCharacteristic(characteristic, new byte[]{0}, WRITE_TYPE_DEFAULT);

        verify(gatt).writeCharacteristic(any(BluetoothGattCharacteristic.class));

        byte[] originalByteArray = new byte[]{0x01};
        characteristic.setValue(originalByteArray);
        callback.onCharacteristicWrite(gatt, characteristic, 0);

        ArgumentCaptor<BluetoothPeripheral> captorPeripheral = ArgumentCaptor.forClass(BluetoothPeripheral.class);
        ArgumentCaptor<byte[]> captorValue = ArgumentCaptor.forClass(byte[].class);
        ArgumentCaptor<BluetoothGattCharacteristic> captorCharacteristic = ArgumentCaptor.forClass(BluetoothGattCharacteristic.class);
        ArgumentCaptor<Integer> captorStatus = ArgumentCaptor.forClass(Integer.class);
        verify(peripheralCallback).onCharacteristicWrite(captorPeripheral.capture(), captorValue.capture(), captorCharacteristic.capture(), captorStatus.capture());

        byte[] value = captorValue.getValue();
        assertEquals(0, value[0]);  // Check if original value is returned and not the one in the characteristic
        assertEquals(peripheral, captorPeripheral.getValue());
        assertEquals(characteristic, captorCharacteristic.getValue());
        assertEquals(GATT_SUCCESS, (int) captorStatus.getValue() );
    }


    @Test
    public void writeCharacteristicNotConnectedTest() throws Exception {
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), PROPERTY_WRITE,0);

        when(gatt.writeCharacteristic(characteristic)).thenReturn(true);

        peripheral.writeCharacteristic(characteristic, new byte[]{0}, WRITE_TYPE_DEFAULT);

        verify(gatt, never()).writeCharacteristic(any(BluetoothGattCharacteristic.class));
    }

    @Test
    public void writeCharacteristicNotWritePropertyTest() throws Exception {
        peripheral.connect();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), 0,0);

        peripheral.writeCharacteristic(characteristic, new byte[] { 0, 0 }, WRITE_TYPE_DEFAULT);

        verify(gatt, never()).writeCharacteristic(any(BluetoothGattCharacteristic.class));
    }

    @Test
    public void writeCharacteristicNoValueTest() throws Exception {
        peripheral.connect();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), PROPERTY_WRITE,0);

        when(gatt.writeCharacteristic(characteristic)).thenReturn(true);

        peripheral.writeCharacteristic(characteristic, null, WRITE_TYPE_DEFAULT);

        verify(gatt, never()).writeCharacteristic(any(BluetoothGattCharacteristic.class));
    }

    @Test
    public void queueTestConsecutiveReads() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        BluetoothGattCharacteristic characteristic2 = new BluetoothGattCharacteristic(UUID.fromString("00002A1E-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        service.addCharacteristic(characteristic);
        service.addCharacteristic(characteristic2);
        byte[] byteArray = new byte[] {0x01, 0x02, 0x03};
        characteristic.setValue(byteArray);
        characteristic2.setValue(byteArray);
        when(gatt.readCharacteristic(characteristic)).thenReturn(true);

        peripheral.readCharacteristic(characteristic);
        peripheral.readCharacteristic(characteristic2);

        verify(gatt).readCharacteristic(characteristic);

        callback.onCharacteristicRead(gatt, characteristic, GATT_SUCCESS);

        verify(peripheralCallback).onCharacteristicUpdate(peripheral, byteArray, characteristic, 0);

        verify(gatt).readCharacteristic(characteristic2);

        callback.onCharacteristicRead(gatt, characteristic2, GATT_SUCCESS);

        verify(peripheralCallback).onCharacteristicUpdate(peripheral, byteArray, characteristic2, 0);
    }

    @Test
    public void queueTestConsecutiveReadsWithError() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        BluetoothGattCharacteristic characteristic2 = new BluetoothGattCharacteristic(UUID.fromString("00002A1E-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        service.addCharacteristic(characteristic);
        service.addCharacteristic(characteristic2);
        byte[] byteArray = new byte[] {0x01, 0x02, 0x03};
        characteristic.setValue(byteArray);
        characteristic2.setValue(byteArray);
        when(gatt.readCharacteristic(characteristic)).thenReturn(true);

        peripheral.readCharacteristic(characteristic);
        peripheral.readCharacteristic(characteristic2);

        verify(gatt).readCharacteristic(characteristic);

        callback.onCharacteristicRead(gatt, characteristic, 128);

        verify(peripheralCallback, never()).onCharacteristicUpdate(peripheral, byteArray, characteristic,128);

        verify(gatt).readCharacteristic(characteristic2);

        callback.onCharacteristicRead(gatt, characteristic2, GATT_SUCCESS);

        verify(peripheralCallback).onCharacteristicUpdate(peripheral, byteArray ,characteristic2, 0);
    }

    @Test
    public void queueTestConsecutiveReadsNoResponse() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        BluetoothGattCharacteristic characteristic2 = new BluetoothGattCharacteristic(UUID.fromString("00002A1E-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        service.addCharacteristic(characteristic);
        service.addCharacteristic(characteristic2);
        byte[] byteArray = new byte[] {0x01, 0x02, 0x03};
        characteristic.setValue(byteArray);
        characteristic2.setValue(byteArray);

        when(gatt.readCharacteristic(characteristic)).thenReturn(true);

        peripheral.readCharacteristic(characteristic);
        peripheral.readCharacteristic(characteristic2);

        verify(gatt).readCharacteristic(characteristic);
        verify(peripheralCallback, never()).onCharacteristicUpdate(peripheral, byteArray, characteristic, 0);
        verify(gatt, never()).readCharacteristic(characteristic2);
    }

    @Test
    public void notifyTest() {
        BluetoothGattCallback callback = connectAndGetCallback();

        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"), PROPERTY_INDICATE,0);
        peripheral.setNotify(characteristic, true);

        byte[] originalByteArray = new byte[]{0x01};
        characteristic.setValue(originalByteArray);
        callback.onCharacteristicChanged(gatt, characteristic);

        ArgumentCaptor<BluetoothPeripheral> captorPeripheral = ArgumentCaptor.forClass(BluetoothPeripheral.class);
        ArgumentCaptor<byte[]> captorValue = ArgumentCaptor.forClass(byte[].class);
        ArgumentCaptor<BluetoothGattCharacteristic> captorCharacteristic = ArgumentCaptor.forClass(BluetoothGattCharacteristic.class);
        ArgumentCaptor<Integer> captorStatus = ArgumentCaptor.forClass(Integer.class);
        verify(peripheralCallback).onCharacteristicUpdate(captorPeripheral.capture(), captorValue.capture(), captorCharacteristic.capture(), captorStatus.capture());

        byte[] value = captorValue.getValue();
        assertEquals(0x01, value[0]);  // Check if original value is returned and not the one in the characteristic
        assertNotEquals(value, originalByteArray);   // Check if the byte array has been copied
        assertEquals(peripheral, captorPeripheral.getValue());
        assertEquals(characteristic, captorCharacteristic.getValue());
        assertEquals(GATT_SUCCESS, (int) captorStatus.getValue() );
    }

    @Test
    public void readDescriptor() {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_INDICATE,0);
        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID.fromString("00002903-0000-1000-8000-00805f9b34fb"),0);
        service.addCharacteristic(characteristic);
        characteristic.addDescriptor(descriptor);

        when(gatt.getServices()).thenReturn(Arrays.asList(service));

        peripheral.readDescriptor(descriptor);

        verify(gatt).readDescriptor(descriptor);

        byte[] originalByteArray = new byte[]{0x01};
        descriptor.setValue(originalByteArray);
        callback.onDescriptorRead(gatt, descriptor, 0);

        ArgumentCaptor<BluetoothPeripheral> captorPeripheral = ArgumentCaptor.forClass(BluetoothPeripheral.class);
        ArgumentCaptor<byte[]> captorValue = ArgumentCaptor.forClass(byte[].class);
        ArgumentCaptor<BluetoothGattDescriptor> captorDescriptor = ArgumentCaptor.forClass(BluetoothGattDescriptor.class);
        ArgumentCaptor<Integer> captorStatus = ArgumentCaptor.forClass(Integer.class);
        verify(peripheralCallback).onDescriptorRead(captorPeripheral.capture(), captorValue.capture(), captorDescriptor.capture(), captorStatus.capture());

        byte[] value = captorValue.getValue();
        assertEquals(0x01, value[0]);
        assertNotEquals(value, originalByteArray);   // Check if the byte array has been copied
        assertEquals(peripheral, captorPeripheral.getValue());
        assertEquals(descriptor, captorDescriptor.getValue());
        assertEquals(GATT_SUCCESS, (int) captorStatus.getValue() );
    }


    @Test
    public void writeDescriptor() {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_INDICATE,0);
        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID.fromString("00002903-0000-1000-8000-00805f9b34fb"),0);
        service.addCharacteristic(characteristic);
        characteristic.addDescriptor(descriptor);

        when(gatt.getServices()).thenReturn(Arrays.asList(service));
        byte[] originalByteArray = new byte[]{0x01};
        peripheral.writeDescriptor(descriptor, originalByteArray);

        verify(gatt).writeDescriptor(descriptor);

        callback.onDescriptorWrite(gatt, descriptor, 0);

        ArgumentCaptor<BluetoothPeripheral> captorPeripheral = ArgumentCaptor.forClass(BluetoothPeripheral.class);
        ArgumentCaptor<byte[]> captorValue = ArgumentCaptor.forClass(byte[].class);
        ArgumentCaptor<BluetoothGattDescriptor> captorDescriptor = ArgumentCaptor.forClass(BluetoothGattDescriptor.class);
        ArgumentCaptor<Integer> captorStatus = ArgumentCaptor.forClass(Integer.class);
        verify(peripheralCallback).onDescriptorWrite(captorPeripheral.capture(), captorValue.capture(), captorDescriptor.capture(), captorStatus.capture());

        byte[] value = captorValue.getValue();
        assertEquals(0x01, value[0]);
        assertNotEquals(value, originalByteArray);   // Check if the byte array has been copied
        assertEquals(peripheral, captorPeripheral.getValue());
        assertEquals(descriptor, captorDescriptor.getValue());
        assertEquals(GATT_SUCCESS, (int) captorStatus.getValue() );
    }

    @Test
    public void readRemoteRSSITest() {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        peripheral.readRemoteRssi();

        verify(gatt).readRemoteRssi();

        callback.onReadRemoteRssi(gatt, -40, 0);

        verify(peripheralCallback).onReadRemoteRssi(peripheral, -40, 0);
    }

    @Test
    public void requestMTUTest() {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        peripheral.requestMtu(32);

        verify(gatt).requestMtu(32);

        callback.onMtuChanged(gatt, 32, 0);

        verify(peripheralCallback).onMtuChanged(peripheral, 32, 0);
    }

    @Test
    public void connectionPriorityTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        peripheral.requestConnectionPriority(CONNECTION_PRIORITY_HIGH);

        verify(gatt).requestConnectionPriority(CONNECTION_PRIORITY_HIGH);
    }

    @Test
    public void createBondWhileConnectedTest() {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        peripheral.createBond();

        verify(device).createBond();
    }

    @Test
    public void createBondWhileUnConnectedTest() {

        peripheral.createBond();

        verify(device).createBond();
    }

    @Test
    public void queueTestRetryCommand() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();
        callback.onConnectionStateChange(gatt, GATT_SUCCESS, STATE_CONNECTED);

        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, 0);
        BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        BluetoothGattCharacteristic characteristic2 = new BluetoothGattCharacteristic(UUID.fromString("00002A1E-0000-1000-8000-00805f9b34fb"),PROPERTY_READ,0);
        service.addCharacteristic(characteristic);
        service.addCharacteristic(characteristic2);
        when(gatt.readCharacteristic(characteristic)).thenReturn(true);
        when(gatt.getServices()).thenReturn(Arrays.asList(service));

        peripheral.readCharacteristic(characteristic);

        verify(gatt).readCharacteristic(characteristic);

        // Trigger bonding to start
        callback.onCharacteristicRead(gatt, characteristic, 5);

        Field field = BluetoothPeripheral.class.getDeclaredField("bondStateReceiver");
        field.setAccessible(true);
        BroadcastReceiver broadcastReceiver = (BroadcastReceiver) field.get(peripheral);

        Intent intent = mock(Intent.class);
        when(intent.getAction()).thenReturn(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        when(intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)).thenReturn(BOND_BONDED);
        when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(device);

        broadcastReceiver.onReceive(context, intent);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        verify(gatt, times(2)).readCharacteristic(characteristic);
    }

    @Test
    public void onConnectionStateChangedConnectedUnbondedTest() throws Exception {
        when(device.getBondState()).thenReturn(BOND_NONE);

        BluetoothGattCallback callback = connectAndGetCallback();

        callback.onConnectionStateChange(gatt, GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);

        verify(gatt).discoverServices();
    }

    @Test
    public void onConnectionStateChangedConnectedGattFailedTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();

        callback.onConnectionStateChange(gatt, GATT_FAILURE, STATE_DISCONNECTED);

        verify(gatt).close();
    }

    @Test
    public void onConnectionStateChangedConnectedAlreadyBondedTest() throws Exception {
        when(device.getBondState()).thenReturn(BOND_BONDED);

        BluetoothGattCallback callback = connectAndGetCallback();

        callback.onConnectionStateChange(gatt, GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        verify(gatt).discoverServices();
    }

    @Test
    public void onConnectionStateChangedConnectedBondedingTest() throws Exception {
        when(device.getBondState()).thenReturn(BOND_BONDING);

        BluetoothGattCallback callback = connectAndGetCallback();

        callback.onConnectionStateChange(gatt, GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        verify(gatt, never()).discoverServices();
    }

    @Test
    public void onConnectionStateChangedDisconnectedTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();

        callback.onConnectionStateChange(gatt, GATT_SUCCESS, BluetoothProfile.STATE_DISCONNECTED);

        verify(gatt).close();
        verify(internalCallback).disconnected(any(BluetoothPeripheral.class), anyInt());
    }

    @Test
    public void onServicesDiscoveredTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();

        BluetoothGattService service = mock(BluetoothGattService.class);
        when(service.getUuid()).thenReturn(SERVICE_UUID);
        when(gatt.getServices()).thenReturn(Arrays.asList(service));

        BluetoothGattCharacteristic characteristic = mock(BluetoothGattCharacteristic.class);
        when(characteristic.getProperties()).thenReturn(PROPERTY_READ);
        when(service.getCharacteristics()).thenReturn(Arrays.asList(characteristic));

        callback.onServicesDiscovered(gatt, 0);

        List<UUID> expected = Arrays.asList(SERVICE_UUID);

//        assertThat(peripheral.getServices(), is(expected));
    }

    @Test
    public void onServicesDiscoveredCharacteristicNotifyTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();

        BluetoothGattService service = mock(BluetoothGattService.class);
        when(service.getUuid()).thenReturn(SERVICE_UUID);
        when(gatt.getServices()).thenReturn(Arrays.asList(service));

        BluetoothGattCharacteristic characteristic = mock(BluetoothGattCharacteristic.class);
        when(characteristic.getProperties()).thenReturn(PROPERTY_NOTIFY);
        when(service.getCharacteristics()).thenReturn(Arrays.asList(characteristic));

        callback.onServicesDiscovered(gatt, 0);

        List<UUID> expected = Arrays.asList(SERVICE_UUID);

 //       assertThat(peripheral.getServices(), is(expected));
    }

    @Test
    public void onServicesDiscoveredCharacteristicIndicateTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();

        BluetoothGattService service = mock(BluetoothGattService.class);
        when(service.getUuid()).thenReturn(SERVICE_UUID);
        when(gatt.getServices()).thenReturn(Arrays.asList(service));

        BluetoothGattCharacteristic characteristic = mock(BluetoothGattCharacteristic.class);
        when(characteristic.getProperties()).thenReturn(PROPERTY_INDICATE);
        when(service.getCharacteristics()).thenReturn(Arrays.asList(characteristic));

        callback.onServicesDiscovered(gatt, 0);

        List<UUID> expected = Arrays.asList(SERVICE_UUID);

//        assertThat(peripheral.getServices(), is(expected));
    }

    @Test
    public void onServicesDiscoveredServicesNotFoundTest() throws Exception {
        BluetoothGattCallback callback = connectAndGetCallback();

        callback.onServicesDiscovered(gatt, 129);

        verify(gatt).disconnect();
    }

    @Test
    public void onBondStateChangedBonded() throws Exception {
        connectAndGetCallback();

        Field field = BluetoothPeripheral.class.getDeclaredField("bondStateReceiver");
        field.setAccessible(true);
        BroadcastReceiver broadcastReceiver = (BroadcastReceiver) field.get(peripheral);

        Intent intent = mock(Intent.class);
        when(intent.getAction()).thenReturn(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        when(intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)).thenReturn(BOND_BONDED);
        when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(device);

        broadcastReceiver.onReceive(context, intent);

        verify(gatt).discoverServices();
    }

    @Test
    public void onBondStateChangedNone() throws Exception {
        connectAndGetCallback();

        Field field = BluetoothPeripheral.class.getDeclaredField("bondStateReceiver");
        field.setAccessible(true);
        BroadcastReceiver broadcastReceiver = (BroadcastReceiver) field.get(peripheral);

        Intent intent = mock(Intent.class);
        when(intent.getAction()).thenReturn(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        when(intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)).thenReturn(BluetoothDevice.BOND_NONE);
        when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(device);

        broadcastReceiver.onReceive(context, intent);

        verify(gatt).disconnect();
    }


    private BluetoothGattCallback connectAndGetCallback() {
        peripheral.connect();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        ArgumentCaptor<BluetoothGattCallback> bluetoothGattCallbackCaptor = ArgumentCaptor.forClass(BluetoothGattCallback.class);
        verify(device).connectGatt(any(Context.class), anyBoolean(), bluetoothGattCallbackCaptor.capture(), anyInt());

        List<BluetoothGattCallback> capturedGatts = bluetoothGattCallbackCaptor.getAllValues();
        return capturedGatts.get(0);
    }
}