/*
 * MIT License
 *
 * Copyright (c) 2017 Inova IT
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package si.inova.neatle.operation;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.os.Build;

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

import java.util.UUID;

import si.inova.neatle.BuildConfig;
import si.inova.neatle.Device;
import si.inova.neatle.Neatle;

import static junit.framework.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.N_MR1)
public class SubscribeCommandTest {

    private UUID serviceUUID = Neatle.createUUID(1);
    private UUID characteristicUUID = Neatle.createUUID(2);
    private UUID clientCharacteristicConfig = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    @Mock
    private CommandObserver commandObserver;
    @Mock
    private CommandObserver operationCommandObserver;
    @Mock
    private BluetoothGatt gatt;
    @Mock
    private Device device;
    @Mock
    private BluetoothGattService gattService;
    @Mock
    private BluetoothGattCharacteristic gattCharacteristic;
    @Mock
    private BluetoothGattDescriptor gattDescriptor;

    private SubscribeCommand subscribeCommand;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        subscribeCommand = new SubscribeCommand(
                SubscribeCommand.Type.SUBSCRIBE_NOTIFICATION,
                serviceUUID,
                characteristicUUID,
                commandObserver);
    }

    @Test
    public void testServiceNotFound() {
        when(gatt.getService(eq(serviceUUID))).thenReturn(null);
        subscribeCommand.execute(device, operationCommandObserver, gatt);
        verifyCommandFail(subscribeCommand, Command.SERVICE_NOT_FOUND);
    }

    @Test
    public void testCharacteristicNotFound() {
        when(gatt.getService(eq(serviceUUID))).thenReturn(gattService);

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        verifyCommandFail(subscribeCommand, Command.CHARACTERISTIC_NOT_FOUND);
    }

    @Test
    public void testDescriptorNotFound() {
        when(gatt.getService(eq(serviceUUID))).thenReturn(gattService);
        when(gattService.getCharacteristic(characteristicUUID)).thenReturn(gattCharacteristic);
        when(gattCharacteristic.getDescriptor(eq(clientCharacteristicConfig))).thenReturn(null);

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        verifyCommandFail(subscribeCommand, Command.DESCRIPTOR_NOT_FOUND);
    }

    @Test
    public void testSubscribeNotificationSuccess() {
        when(gattDescriptor.getValue()).thenReturn(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        checkCommandSuccess(SubscribeCommand.Type.SUBSCRIBE_NOTIFICATION);
    }

    @Test
    public void testSubscribeIndicationSuccess() {
        when(gattDescriptor.getValue()).thenReturn(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
        checkCommandSuccess(SubscribeCommand.Type.SUBSCRIBE_INDICATION);
    }

    @Test
    public void testUnsubscribeSuccess() {
        when(gattDescriptor.getValue()).thenReturn(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
        checkCommandSuccess(SubscribeCommand.Type.UNSUBSCRIBE);
    }

    @Test
    public void testCharacteristicNotificationFail() {
        when(gatt.getService(eq(serviceUUID))).thenReturn(gattService);
        when(gattService.getCharacteristic(characteristicUUID)).thenReturn(gattCharacteristic);
        when(gattCharacteristic.getDescriptor(eq(clientCharacteristicConfig))).thenReturn(gattDescriptor);
        when(gatt.setCharacteristicNotification(Mockito.eq(gattCharacteristic), Mockito.anyBoolean())).thenReturn(false);

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        verifyCommandFail(subscribeCommand, BluetoothGatt.GATT_FAILURE);
    }

    @Test
    public void testUnsubscribeWithListeners() {
        when(device.getCharacteristicsChangedListenerCount(characteristicUUID)).thenReturn(1);

        subscribeCommand = new SubscribeCommand(
                SubscribeCommand.Type.UNSUBSCRIBE,
                serviceUUID,
                characteristicUUID,
                commandObserver);

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        CommandResult result = CommandResult.createEmptySuccess(characteristicUUID);
        verify(commandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
        verify(operationCommandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
    }

    @Test
    public void testWriteDescriptorFail() {
        when(gatt.getService(eq(serviceUUID))).thenReturn(gattService);
        when(gattService.getCharacteristic(characteristicUUID)).thenReturn(gattCharacteristic);
        when(gattCharacteristic.getDescriptor(eq(clientCharacteristicConfig))).thenReturn(gattDescriptor);
        when(gatt.setCharacteristicNotification(Mockito.eq(gattCharacteristic), Mockito.anyBoolean())).thenReturn(true);
        when(gatt.writeDescriptor(gattDescriptor)).thenReturn(false);

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        verifyCommandFail(subscribeCommand, BluetoothGatt.GATT_FAILURE);
    }

    @Test
    public void testWriteDescriptorSuccess() {
        setupDescriptorSuccess();

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        verify(commandObserver, times(0)).finished(Mockito.any(Command.class), Mockito.any(CommandResult.class));
        verify(operationCommandObserver, times(0)).finished(Mockito.any(Command.class), Mockito.any(CommandResult.class));
    }

    @Test
    public void testOnDescriptorWriteSuccess() {
        setupDescriptorSuccess();

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        subscribeCommand.onDescriptorWrite(gatt, gattDescriptor, BluetoothGatt.GATT_SUCCESS);

        CommandResult result = CommandResult.createEmptySuccess(characteristicUUID);
        verify(commandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
        verify(operationCommandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
    }

    @Test
    public void testOnDescriptorWriteFail() {
        setupDescriptorSuccess();

        subscribeCommand.execute(device, operationCommandObserver, gatt);
        subscribeCommand.onDescriptorWrite(gatt, gattDescriptor, BluetoothGatt.GATT_FAILURE);
        verifyCommandFail(subscribeCommand, BluetoothGatt.GATT_FAILURE);
    }

    @Test
    public void testOnError() {
        setupDescriptorSuccess();
        subscribeCommand.execute(device, operationCommandObserver, gatt);

        subscribeCommand.onError(BluetoothGatt.GATT_FAILURE);
        verifyCommandFail(subscribeCommand, BluetoothGatt.GATT_FAILURE);
    }

    @Test
    public void testOnStringBecauseWhyNot() {
        SubscribeCommand subNotif = new SubscribeCommand(
                SubscribeCommand.Type.SUBSCRIBE_NOTIFICATION,
                serviceUUID,
                characteristicUUID,
                commandObserver);

        SubscribeCommand subIndi = new SubscribeCommand(
                SubscribeCommand.Type.SUBSCRIBE_INDICATION,
                serviceUUID,
                characteristicUUID,
                commandObserver);

        SubscribeCommand unsub = new SubscribeCommand(
                SubscribeCommand.Type.UNSUBSCRIBE,
                serviceUUID,
                characteristicUUID,
                commandObserver);


        assertNotNull(subNotif.toString());
        assertNotNull(subIndi.toString());
        assertNotNull(unsub.toString());
    }

    public void checkCommandSuccess(@SubscribeCommand.Type int type) {
        subscribeCommand.execute(device, operationCommandObserver, gatt);
        when(gatt.getService(eq(serviceUUID))).thenReturn(gattService);
        when(gattService.getCharacteristic(characteristicUUID)).thenReturn(gattCharacteristic);
        when(gattCharacteristic.getDescriptor(eq(clientCharacteristicConfig))).thenReturn(gattDescriptor);

        subscribeCommand = new SubscribeCommand(
                type,
                serviceUUID,
                characteristicUUID,
                commandObserver);

        subscribeCommand.execute(device, operationCommandObserver, gatt);

        CommandResult result = CommandResult.createEmptySuccess(characteristicUUID);
        verify(commandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
        verify(operationCommandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
    }

    private void setupDescriptorSuccess() {
        when(gatt.getService(eq(serviceUUID))).thenReturn(gattService);
        when(gattService.getCharacteristic(characteristicUUID)).thenReturn(gattCharacteristic);
        when(gattCharacteristic.getDescriptor(eq(clientCharacteristicConfig))).thenReturn(gattDescriptor);
        when(gatt.setCharacteristicNotification(Mockito.eq(gattCharacteristic), Mockito.anyBoolean())).thenReturn(true);
        when(gatt.writeDescriptor(gattDescriptor)).thenReturn(true);
    }

    private void verifyCommandFail(SubscribeCommand subscribeCommand, int failCode) {
        CommandResult result = CommandResult.createErrorResult(characteristicUUID, failCode);
        verify(commandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
        verify(operationCommandObserver, times(1)).finished(eq(subscribeCommand), refEq(result, "timestamp"));
    }
}