/**
 * Copyright (c) 2020 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.hono.deviceconnection.infinispan.client;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import org.eclipse.hono.client.ClientErrorException;
import org.eclipse.hono.client.ServiceInvocationException;
import org.eclipse.hono.util.Constants;
import org.eclipse.hono.util.DeviceConnectionConstants;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;

import io.opentracing.Span;
import io.opentracing.SpanContext;
import io.opentracing.Tracer;
import io.opentracing.tag.Tag;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.junit5.Timeout;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;

/**
 * Tests verifying behavior of {@link CacheBasedDeviceConnectionInfo}.
 *
 */
@ExtendWith(VertxExtension.class)
@Timeout(value = 5, timeUnit = TimeUnit.SECONDS)
class CacheBasedDeviceConnectionInfoTest {

    private static final String PARAMETERIZED_TEST_NAME_PATTERN = "{displayName} [{index}]; parameters: {argumentsWithNames}";

    private DeviceConnectionInfo info;
    private BasicCache<String, String> cache;
    private Tracer tracer;
    private Span span;

    static Stream<Set<String>> extraUnusedViaGateways() {
        return Stream.of(
                Collections.emptySet(),
                getViaGatewaysExceedingThreshold()
        );
    }

    @SuppressWarnings("unchecked")
    private static <K, V> Cache<K, V> mockCache() {
        return mock(Cache.class);
    }

    private static Set<String> getViaGatewaysExceedingThreshold() {
        final HashSet<String> set = new HashSet<>();
        for (int i = 0; i < CacheBasedDeviceConnectionInfo.VIA_GATEWAYS_OPTIMIZATION_THRESHOLD + 1; i++) {
            set.add("gw#" + i);
        }
        return set;
    }

    /**
     * Sets up the fixture.
     */
    @SuppressWarnings("unchecked")
    @BeforeEach
    void setUp(final Vertx vertx, final VertxTestContext testContext) {

        final var cacheManager = new DefaultCacheManager(false);
        cacheManager.defineConfiguration("cache-name", new ConfigurationBuilder()
                .build());
        cache = new EmbeddedCache<>(vertx, cacheManager, "cache-name", "foo", "bar");
        cache.connect().onComplete(testContext.completing());

        final SpanContext spanContext = mock(SpanContext.class);
        span = mock(Span.class);
        when(span.context()).thenReturn(spanContext);
        final Tracer.SpanBuilder spanBuilder = mock(Tracer.SpanBuilder.class, Mockito.RETURNS_SMART_NULLS);
        when(spanBuilder.addReference(anyString(), any())).thenReturn(spanBuilder);
        when(spanBuilder.withTag(anyString(), anyBoolean())).thenReturn(spanBuilder);
        when(spanBuilder.withTag(anyString(), (String) any())).thenReturn(spanBuilder);
        when(spanBuilder.withTag(anyString(), (Number) any())).thenReturn(spanBuilder);
        when(spanBuilder.withTag(any(Tag.class), any())).thenReturn(spanBuilder);
        when(spanBuilder.ignoreActiveSpan()).thenReturn(spanBuilder);
        when(spanBuilder.start()).thenReturn(span);
        tracer = mock(Tracer.class);
        when(tracer.buildSpan(anyString())).thenReturn(spanBuilder);

        info = new CacheBasedDeviceConnectionInfo(cache, tracer);
    }

    /**
     * Verifies that a last known gateway can be successfully set.
     *
     * @param ctx The vert.x test context.
     */
    @Test
    void testSetLastKnownGatewaySucceeds(final VertxTestContext ctx) {
        final Cache<String, String> mockedCache = mockCache();
        info = new CacheBasedDeviceConnectionInfo(mockedCache, tracer);
        when(mockedCache.put(anyString(), anyString(), anyLong(), any())).thenReturn(Future.succeededFuture("oldValue"));
        info.setLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, "device-id", "gw-id", mock(Span.class))
            .onComplete(ctx.completing());
    }

    /**
     * Verifies that a request to set a gateway fails with a {@link org.eclipse.hono.client.ServiceInvocationException}.
     *
     * @param ctx The vert.x test context.
     */
    @Test
    void testSetLastKnownGatewayFails(final VertxTestContext ctx) {
        final Cache<String, String> mockedCache = mockCache();
        info = new CacheBasedDeviceConnectionInfo(mockedCache, tracer);
        when(mockedCache.put(anyString(), anyString(), anyLong(), any())).thenReturn(Future.failedFuture(new IOException("not available")));
        info.setLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, "device-id", "gw-id", mock(Span.class))
            .onComplete(ctx.failing(t -> {
                ctx.verify(() -> assertThat(t).isInstanceOf(ServiceInvocationException.class));
                ctx.completeNow();
            }));
    }

    /**
     * Verifies that a last known gateway can be successfully retrieved.
     *
     * @param ctx The vert.x test context.
     */
    @Test
    void testGetLastKnownGatewaySucceeds(final VertxTestContext ctx) {
        final Cache<String, String> mockedCache = mockCache();
        info = new CacheBasedDeviceConnectionInfo(mockedCache, tracer);
        when(mockedCache.get(anyString())).thenReturn(Future.succeededFuture("gw-id"));
        info.getLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, "device-id", mock(Span.class))
            .onComplete(ctx.succeeding(value -> {
                ctx.verify(() -> {
                    assertThat(value.getString(DeviceConnectionConstants.FIELD_GATEWAY_ID)).isEqualTo("gw-id");
                });
                ctx.completeNow();
            }));
    }

    /**
     * Verifies that a request to get a gateway fails with a {@link org.eclipse.hono.client.ServiceInvocationException}
     * if the remote cache cannot be accessed.
     *
     * @param ctx The vert.x test context.
     */
    @Test
    void testGetLastKnownGatewayFailsForCacheAccessException(final VertxTestContext ctx) {
        final Cache<String, String> mockedCache = mockCache();
        info = new CacheBasedDeviceConnectionInfo(mockedCache, tracer);
        when(mockedCache.get(anyString())).thenReturn(Future.failedFuture(new IOException("not available")));
        info.getLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, "device-id", mock(Span.class))
            .onComplete(ctx.failing(t -> {
                ctx.verify(() -> {
                    assertThat(t).isInstanceOf(ServiceInvocationException.class);
                    assertThat(((ServiceInvocationException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_INTERNAL_ERROR);
                });
                ctx.completeNow();
            }));
    }

    /**
     * Verifies that a request to get a gateway fails with a {@link org.eclipse.hono.client.ClientErrorException}
     * if no entry exists for the given device.
     *
     * @param ctx The vert.x test context.
     */
    @Test
    void testGetLastKnownGatewayFailsForNonExistingEntry(final VertxTestContext ctx) {
        final Cache<String, String> mockedCache = mockCache();
        info = new CacheBasedDeviceConnectionInfo(mockedCache, tracer);
        when(mockedCache.get(anyString())).thenReturn(Future.succeededFuture(null));
        info.getLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, "device-id", mock(Span.class))
            .onComplete(ctx.failing(t -> {
                ctx.verify(() -> {
                    assertThat(t).isInstanceOf(ClientErrorException.class);
                    assertThat(((ServiceInvocationException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
                });
                ctx.completeNow();
            }));
    }

    /**
     * Verifies that the <em>setCommandHandlingAdapterInstance</em> operation succeeds.
     *
     * @param ctx The vert.x context.
     */
    @Test
    public void testSetCommandHandlingAdapterInstanceSucceeds(final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, null, span)
                .onComplete(ctx.succeeding(result -> ctx.completeNow()));
    }

    /**
     * Verifies that the <em>setCommandHandlingAdapterInstance</em> operation succeeds
     * with a non-negative lifespan given.
     *
     * @param ctx The vert.x context.
     */
    @Test
    public void testSetCommandHandlingAdapterInstanceWithLifespanSucceeds(final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, Duration.ofSeconds(10), span)
                .onComplete(ctx.succeeding(result -> ctx.completeNow()));
    }

    /**
     * Verifies that the <em>removeCommandHandlingAdapterInstance</em> operation succeeds if there was an entry to be deleted.
     *
     * @param ctx The vert.x context.
     */
    @Test
    public void testRemoveCommandHandlingAdapterInstanceSucceeds(final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, null, span)
        .compose(v -> info.removeCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId,
                adapterInstance, span))
        .onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertThat(result).isTrue();
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>removeCommandHandlingAdapterInstance</em> operation result is <em>false</em> if
     * no entry was registered for the device. Only an adapter instance for another device of the tenant was
     * registered.
     *
     * @param ctx The vert.x context.
     */
    @Test
    public void testRemoveCommandHandlingAdapterInstanceFailsForOtherDevice(final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, null, span)
        .compose(v -> {
            return info.removeCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, "otherDevice", adapterInstance, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertThat(result).isFalse();
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>removeCommandHandlingAdapterInstance</em> operation result is <em>false</em> if
     * the given adapter instance parameter doesn't match the one of the entry registered for the given device.
     *
     * @param ctx The vert.x context.
     */
    @Test
    public void testRemoveCommandHandlingAdapterInstanceFailsForOtherAdapterInstance(final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, null, span)
        .compose(v -> {
            return info.removeCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, "otherAdapterInstance", span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertThat(result).isFalse();
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds if an adapter instance had
     * been registered for the given device.
     *
     * @param ctx The vert.x context.
     */
    @Test
    public void testGetCommandHandlingAdapterInstancesForDevice(final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, null, span)
        .compose(v -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, Collections.emptySet(), span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, deviceId, adapterInstance);
            assertGetInstancesResultSize(result, 1);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation fails
     * if the adapter instance mapping entry has expired.
     *
     * @param vertx The vert.x instance.
     * @param ctx The vert.x context.
     */
    @Test
    public void testGetCommandHandlingAdapterInstancesWithExpiredEntry(final Vertx vertx, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";

        final Cache<String, String> mockedCache = spy(cache);
        info = new CacheBasedDeviceConnectionInfo(mockedCache, tracer);
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, Duration.ofMillis(1), span)
        .compose(v -> {
            final Promise<JsonObject> instancesPromise = Promise.promise();
            // wait 2ms to make sure entry has expired after that
            vertx.setTimer(2, tid -> {
                info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId,
                        Collections.emptySet(), span)
                        .onComplete(instancesPromise.future());
            });
            return instancesPromise.future();
        }).onComplete(ctx.failing(t -> ctx.verify(() -> {
            assertThat(t).isInstanceOf(ServiceInvocationException.class);
            assertThat(((ServiceInvocationException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation fails for a device with no
     * viaGateways, if no matching instance has been registered.
     *
     * @param ctx The vert.x context.
     */
    @Test
    public void testGetCommandHandlingAdapterInstancesFails(final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final Set<String> viaGateways = Collections.emptySet();
        info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span)
                .onComplete(ctx.failing(t -> ctx.verify(() -> {
                    assertThat(t).isInstanceOf(ServiceInvocationException.class);
                    assertThat(((ServiceInvocationException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
                    ctx.completeNow();
                })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds if an adapter instance has
     * been registered for the last known gateway associated with the given device.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesForLastKnownGateway(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String otherAdapterInstance = "otherAdapterInstance";
        final String gatewayId = "gw-1";
        final String otherGatewayId = "gw-2";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId, otherGatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for gateway
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, adapterInstance, null, span)
        .compose(v -> {
            // set command handling adapter instance for other gateway
            return info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, otherGatewayId, otherAdapterInstance, null, span);
        }).compose(deviceConnectionResult -> {
            return info.setLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, deviceId, gatewayId, span);
        }).compose(u -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, gatewayId, adapterInstance);
            // be sure that only the mapping for the last-known-gateway is returned, not the mappings for both via gateways
            assertGetInstancesResultSize(result, 1);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds if multiple adapter instances
     * have been registered for gateways of the given device, but the last known gateway associated with the given
     * device is not in the viaGateways list of the device.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesWithMultiResultAndLastKnownGatewayNotInVia(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String otherAdapterInstance = "otherAdapterInstance";
        final String gatewayId = "gw-1";
        final String otherGatewayId = "gw-2";
        final String gatewayIdNotInVia = "gw-old";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId, otherGatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for gateway
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, adapterInstance, null, span)
        .compose(v -> {
            // set command handling adapter instance for other gateway
            return info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, otherGatewayId, otherAdapterInstance, null, span);
        }).compose(deviceConnectionResult -> {
            return info.setLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, deviceId, gatewayIdNotInVia, span);
        }).compose(u -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, gatewayId, adapterInstance);
            assertGetInstancesResultMapping(result, otherGatewayId, otherAdapterInstance);
            // last-known-gateway is not in via list, therefore no single result is returned
            assertGetInstancesResultSize(result, 2);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds if multiple adapter instances
     * have been registered for gateways of the given device, but the last known gateway associated with the given
     * device has no adapter associated with it.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesWithMultiResultAndNoAdapterForLastKnownGateway(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String otherAdapterInstance = "otherAdapterInstance";
        final String gatewayId = "gw-1";
        final String otherGatewayId = "gw-2";
        final String gatewayWithNoAdapterInstance = "gw-other";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId, otherGatewayId, gatewayWithNoAdapterInstance));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for gateway
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, adapterInstance, null, span)
                .compose(v -> {
                    // set command handling adapter instance for other gateway
                    return info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, otherGatewayId, otherAdapterInstance, null, span);
                }).compose(deviceConnectionResult -> {
            return info.setLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, deviceId, gatewayWithNoAdapterInstance, span);
        }).compose(u -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, gatewayId, adapterInstance);
            assertGetInstancesResultMapping(result, otherGatewayId, otherAdapterInstance);
            // no adapter registered for last-known-gateway, therefore no single result is returned
            assertGetInstancesResultSize(result, 2);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation fails if an adapter instance has
     * been registered for the last known gateway associated with the given device, but that gateway isn't in the
     * given viaGateways set.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesForSingleResultAndLastKnownGatewayNotInVia(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String gatewayId = "gw-1";
        final Set<String> viaGateways = new HashSet<>(Set.of("otherGatewayId"));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for gateway
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, adapterInstance, null, span)
        .compose(deviceConnectionResult -> {
            return info.setLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, deviceId, gatewayId, span);
        }).compose(u -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.failing(t -> ctx.verify(() -> {
            assertThat(t).isInstanceOf(ServiceInvocationException.class);
            assertThat(((ServiceInvocationException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds with a result containing just
     * the mapping of *the given device* to its command handling adapter instance, even though an adapter instance is
     * also registered for the last known gateway associated with the given device.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesWithLastKnownGatewayIsGivingDevicePrecedence(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String otherAdapterInstance = "otherAdapterInstance";
        final String gatewayId = "gw-1";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for device
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, null, span)
        .compose(v -> {
            // set command handling adapter instance for other gateway
            return info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, otherAdapterInstance, null, span);
        }).compose(u -> {
            return info.setLastKnownGatewayForDevice(Constants.DEFAULT_TENANT, deviceId, gatewayId, span);
        }).compose(w -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, deviceId, adapterInstance);
            // be sure that only the mapping for the device is returned, not the mappings for the gateway
            assertGetInstancesResultSize(result, 1);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds with a result containing just
     * the mapping of *the given device* to its command handling adapter instance, even though an adapter instance is
     * also registered for the other gateway given in the viaGateway.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesWithoutLastKnownGatewayIsGivingDevicePrecedence(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String otherAdapterInstance = "otherAdapterInstance";
        final String gatewayId = "gw-1";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for device
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, deviceId, adapterInstance, null, span)
        .compose(v -> {
            // set command handling adapter instance for other gateway
            return info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, otherAdapterInstance, null, span);
        }).compose(w -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, deviceId, adapterInstance);
            // be sure that only the mapping for the device is returned, not the mappings for the gateway
            assertGetInstancesResultSize(result, 1);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds with a result containing
     * the mapping of a gateway, even though there is no last known gateway set for the device.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesForOneSubscribedVia(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String gatewayId = "gw-1";
        final String otherGatewayId = "gw-2";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId, otherGatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for gateway
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, adapterInstance, null, span)
        .compose(v -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, gatewayId, adapterInstance);
            assertGetInstancesResultSize(result, 1);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation succeeds with a result containing
     * the mappings of gateways, even though there is no last known gateway set for the device.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesForMultipleSubscribedVias(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String otherAdapterInstance = "otherAdapterInstance";
        final String gatewayId = "gw-1";
        final String otherGatewayId = "gw-2";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId, otherGatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for gateway
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, gatewayId, adapterInstance, null, span)
        .compose(v -> {
            // set command handling adapter instance for other gateway
            return info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, otherGatewayId, otherAdapterInstance, null, span);
        }).compose(u -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.succeeding(result -> ctx.verify(() -> {
            assertNotNull(result);
            assertGetInstancesResultMapping(result, gatewayId, adapterInstance);
            assertGetInstancesResultMapping(result, otherGatewayId, otherAdapterInstance);
            assertGetInstancesResultSize(result, 2);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation fails for a device with a
     * non-empty set of given viaGateways, if no matching instance has been registered.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesFailsWithGivenGateways(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String gatewayId = "gw-1";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span)
        .onComplete(ctx.failing(t -> ctx.verify(() -> {
            assertThat(t).isInstanceOf(ServiceInvocationException.class);
            assertThat(((ServiceInvocationException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
            ctx.completeNow();
        })));
    }

    /**
     * Verifies that the <em>getCommandHandlingAdapterInstances</em> operation fails if no matching instance
     * has been registered. An adapter instance has been registered for another device of the same tenant though.
     *
     * @param extraUnusedViaGateways Test values.
     * @param ctx The vert.x context.
     */
    @ParameterizedTest(name = PARAMETERIZED_TEST_NAME_PATTERN)
    @MethodSource("extraUnusedViaGateways")
    public void testGetCommandHandlingAdapterInstancesFailsForOtherTenantDevice(final Set<String> extraUnusedViaGateways, final VertxTestContext ctx) {
        final String deviceId = "testDevice";
        final String adapterInstance = "adapterInstance";
        final String gatewayId = "gw-1";
        final String otherGatewayId = "gw-2";
        final Set<String> viaGateways = new HashSet<>(Set.of(gatewayId));
        viaGateways.addAll(extraUnusedViaGateways);
        // set command handling adapter instance for other gateway
        info.setCommandHandlingAdapterInstance(Constants.DEFAULT_TENANT, otherGatewayId, adapterInstance, null, span)
        .compose(v -> {
            return info.getCommandHandlingAdapterInstances(Constants.DEFAULT_TENANT, deviceId, viaGateways, span);
        }).onComplete(ctx.failing(t -> ctx.verify(() -> {
            assertThat(t).isInstanceOf(ServiceInvocationException.class);
            assertThat(((ServiceInvocationException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
            ctx.completeNow();
        })));
    }

    /**
     * Asserts the the given result JSON of the <em>getCommandHandlingAdapterInstances</em> method contains
     * an "adapter-instances" entry with the given device id and adapter instance id.
     */
    private void assertGetInstancesResultMapping(final JsonObject resultJson, final String deviceId, final String adapterInstanceId) {
        assertNotNull(resultJson);
        final JsonArray adapterInstancesJson = resultJson.getJsonArray(DeviceConnectionConstants.FIELD_ADAPTER_INSTANCES);
        assertNotNull(adapterInstancesJson);
        boolean entryFound = false;
        for (int i = 0; i < adapterInstancesJson.size(); i++) {
            final JsonObject entry = adapterInstancesJson.getJsonObject(i);
            if (deviceId.equals(entry.getString(DeviceConnectionConstants.FIELD_PAYLOAD_DEVICE_ID))) {
                entryFound = true;
                assertEquals(adapterInstanceId, entry.getString(DeviceConnectionConstants.FIELD_ADAPTER_INSTANCE_ID));
            }
        }
        assertTrue(entryFound);
    }

    private void assertGetInstancesResultSize(final JsonObject resultJson, final int size) {
        assertNotNull(resultJson);
        final JsonArray adapterInstancesJson = resultJson.getJsonArray(DeviceConnectionConstants.FIELD_ADAPTER_INSTANCES);
        assertNotNull(adapterInstancesJson);
        assertEquals(size, adapterInstancesJson.size());
    }

}