/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.artemis.tests.unit.core.postoffice.impl;

import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;

import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.core.config.WildcardConfiguration;
import org.apache.activemq.artemis.core.filter.Filter;
import org.apache.activemq.artemis.core.persistence.impl.nullpm.NullStorageManager;
import org.apache.activemq.artemis.core.postoffice.Binding;
import org.apache.activemq.artemis.core.postoffice.BindingType;
import org.apache.activemq.artemis.core.postoffice.Bindings;
import org.apache.activemq.artemis.core.postoffice.BindingsFactory;
import org.apache.activemq.artemis.core.postoffice.QueueBinding;
import org.apache.activemq.artemis.core.postoffice.impl.WildcardAddressManager;
import org.apache.activemq.artemis.core.server.Bindable;
import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.core.server.RoutingContext;
import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType;
import org.apache.activemq.artemis.core.server.impl.AddressInfo;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class WildcardAddressManagerUnitTest extends ActiveMQTestBase {
   private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

   @Test
   public void testUnitOnWildCardFailingScenario() throws Exception {
      int errors = 0;
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
      ad.addBinding(new BindingFake("Topic1", "Topic1"));
      ad.addBinding(new BindingFake("Topic1", "one"));
      ad.addBinding(new BindingFake("*", "two"));
      ad.removeBinding(SimpleString.of("one"), null);
      try {
         ad.removeBinding(SimpleString.of("two"), null);
      } catch (Throwable e) {
         // We are not failing the test here as this test is replicating the exact scenario reported by the user in
         // which this would be ignored.
         errors++;
         e.printStackTrace();
      }
      try {
         ad.addBinding(new BindingFake("Topic1", "three"));
      } catch (Throwable e) {
         // We are not failing the test here as this test is replicating the exact scenario reported by the user in
         // which this would be ignored.
         errors++;
         e.printStackTrace();
      }

      assertEquals(0, errors, "Exception happened during the process");
   }

   @Test
   public void testUnitOnWildCardFailingScenarioFQQN() throws Exception {
      int errors = 0;
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
      ad.addBinding(new BindingFake("Topic1", "Topic1"));
      ad.addBinding(new BindingFake("Topic1", "one"));
      ad.addBinding(new BindingFake("*", "two"));
      ad.removeBinding(SimpleString.of("Topic1::one"), null);
      try {
         ad.removeBinding(SimpleString.of("*::two"), null);
      } catch (Throwable e) {
         // We are not failing the test here as this test is replicating the exact scenario reported by the user in
         // which this would be ignored.
         errors++;
         e.printStackTrace();
      }
      try {
         ad.addBinding(new BindingFake("Topic1", "three"));
      } catch (Throwable e) {
         // We are not failing the test here as this test is replicating the exact scenario reported by the user in
         // which this would be ignored.
         errors++;
         e.printStackTrace();
      }

      assertEquals(0, errors, "Exception happened during the process");
   }

   /**
    * Test for ARTEMIS-1610
    */
   @Test
   public void testWildCardAddressRemoval() throws Exception {

      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Queue1.#"), RoutingType.ANYCAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic1.#"), RoutingType.MULTICAST));
      ad.addBinding(new BindingFake("Topic1.#", "two"));
      ad.addBinding(new BindingFake("Queue1.#", "one"));

      //Calling this method will trigger the wildcard to be added to the wildcard map internal
      //to WildcardAddressManager
      assertEquals(1, ad.getBindingsForRoutingAddress(SimpleString.of("Topic1.topic")).getBindings().size());

      //Remove the address
      ad.removeAddressInfo(SimpleString.of("Topic1.#"));

      assertNull(ad.getAddressInfo(SimpleString.of("Topic1.#")));
   }

   @Test
   public void testWildCardAddressRemovalNoExplicitBindings() throws Exception {
      final SimpleString address = SimpleString.of(getName());
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);

      // add a wildcard address & binding
      ad.addAddressInfo(new AddressInfo(SimpleString.of("#"), RoutingType.MULTICAST));
      ad.addBinding(new BindingFake("#", RandomUtil.randomUUIDString()));

      // add a non-wildcard address
      ad.addAddressInfo(new AddressInfo(address, RoutingType.MULTICAST));

      // induce the creation of an bindings object for the non-wildcard address
      ad.getBindingsForRoutingAddress(address);

      // remove the AddressInfo
      ad.removeAddressInfo(address);

      // verify everything is cleaned up
      assertNull(ad.getAddressMap().getRootNode().getChild(address.toString()));
      assertNull(ad.getAddressInfo(address));
      assertNull(ad.getExistingBindingsForRoutingAddress(address));
   }

   @Test
   public void testWildCardAddRemoveBinding() throws Exception {
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
      SimpleString address = SimpleString.of("Queue1.1");
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Queue1.#"), RoutingType.ANYCAST));

      BindingFake bindingFake = new BindingFake("Queue1.#", "one");
      assertTrue(ad.addBinding(bindingFake));

      assertEquals(1, ad.getBindingsForRoutingAddress(address).getBindings().size());

      ad.removeBinding(bindingFake.getUniqueName(), null);

      assertNull(ad.getExistingBindingsForRoutingAddress(address));

   }


   @Test
   public void testWildCardAddAlreadyExistingBindingShouldThrowException() throws Exception {
      assertThrows(ActiveMQQueueExistsException.class, () -> {
         WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
         ad.addAddressInfo(new AddressInfo(SimpleString.of("Queue1.#"), RoutingType.ANYCAST));
         ad.addBinding(new BindingFake("Queue1.#", "one"));
         ad.addBinding(new BindingFake("Queue1.#", "one"));
      });
   }

   @Test
   public void testWildCardAddressRemovalDifferentWildcard() throws Exception {

      final WildcardConfiguration configuration = new WildcardConfiguration();
      configuration.setAnyWords('>');
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), configuration, null, null);
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic1.>"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic1.test"), RoutingType.MULTICAST));
      ad.addBinding(new BindingFake("Topic1.>", "one"));

      assertEquals(1, ad.getExistingBindingsForRoutingAddress(SimpleString.of("Topic1.>")).getBindings().size());
      assertEquals(1, ad.getBindingsForRoutingAddress(SimpleString.of("Topic1.test")).getBindings().size());
      assertEquals(0, ad.getDirectBindings(SimpleString.of("Topic1.test")).size());
      assertEquals(1, ad.getDirectBindings(SimpleString.of("Topic1.>")).size());

      //Remove the address
      ad.removeAddressInfo(SimpleString.of("Topic1.test"));

      //should still have 1 address and binding
      assertEquals(1, ad.getAddresses().size());
      assertEquals(1, ad.getBindings().count());

      ad.removeBinding(SimpleString.of("one"), null);
      ad.removeAddressInfo(SimpleString.of("Topic1.>"));

      assertEquals(0, ad.getAddresses().size());
      assertEquals(0, ad.getBindings().count());
      assertEquals(0, ad.getDirectBindings(SimpleString.of("Topic1.>")).size());
   }

   @Test
   public void testWildCardAddressDirectBindings() throws Exception {

      final WildcardConfiguration configuration = new WildcardConfiguration();
      configuration.setAnyWords('>');
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), configuration, null, null);
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic1.>"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic1.test"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic1.test.test1"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic1.test.test2"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic2.>"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("Topic2.test"), RoutingType.MULTICAST));
      ad.addBinding(new BindingFake("Topic1.>", "one"));
      ad.addBinding(new BindingFake("Topic1.test", "two"));
      ad.addBinding(new BindingFake("Topic2.test", "three"));

      assertEquals(1, ad.getExistingBindingsForRoutingAddress(SimpleString.of("Topic1.>")).getBindings().size());
      assertEquals(2, ad.getBindingsForRoutingAddress(SimpleString.of("Topic1.test")).getBindings().size());
      assertEquals(1, ad.getBindingsForRoutingAddress(SimpleString.of("Topic1.test.test1")).getBindings().size());
      assertEquals(1, ad.getBindingsForRoutingAddress(SimpleString.of("Topic1.test.test2")).getBindings().size());

      assertEquals(1, ad.getDirectBindings(SimpleString.of("Topic1.>")).size());
      assertEquals(1, ad.getDirectBindings(SimpleString.of("Topic1.test")).size());
      assertEquals(0, ad.getDirectBindings(SimpleString.of("Topic1.test1")).size());
      assertEquals(0, ad.getDirectBindings(SimpleString.of("Topic1.test2")).size());
      assertEquals(0, ad.getDirectBindings(SimpleString.of("Topic2.>")).size());
      assertEquals(1, ad.getDirectBindings(SimpleString.of("Topic2.test")).size());

   }

   @Test
   public void testSingleWordWildCardAddressBindingsForRouting() throws Exception {
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
      ad.addAddressInfo(new AddressInfo(SimpleString.of("news.*"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("news.*.sport"), RoutingType.MULTICAST));
      ad.addBinding(new BindingFake("news.*", "one"));
      ad.addBinding(new BindingFake("news.*.sport", "two"));

      Collection<Binding> bindings = ad.getBindingsForRoutingAddress(SimpleString.of("news.europe")).getBindings();
      assertEquals(1, bindings.size());
      assertEquals("one", bindings.iterator().next().getUniqueName().toString());
      bindings = ad.getBindingsForRoutingAddress(SimpleString.of("news.usa")).getBindings();
      assertEquals(1, bindings.size());
      assertEquals("one", bindings.iterator().next().getUniqueName().toString());
      bindings = ad.getBindingsForRoutingAddress(SimpleString.of("news.europe.sport")).getBindings();
      assertEquals(1, bindings.size());
      assertEquals("two", bindings.iterator().next().getUniqueName().toString());
      bindings = ad.getBindingsForRoutingAddress(SimpleString.of("news.usa.sport")).getBindings();
      assertEquals(1, bindings.size());
      assertEquals("two", bindings.iterator().next().getUniqueName().toString());
      assertNull(ad.getBindingsForRoutingAddress(SimpleString.of("news.europe.fr.sport")));
   }

   @Test
   public void testAnyWordsWildCardAddressBindingsForRouting() throws Exception {
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
      ad.addAddressInfo(new AddressInfo(SimpleString.of("news.europe.#"), RoutingType.MULTICAST));
      ad.addBinding(new BindingFake("news.europe.#", "one"));

      assertEquals(1, ad.getBindingsForRoutingAddress(SimpleString.of("news.europe")).getBindings().size());
      assertEquals(1, ad.getBindingsForRoutingAddress(SimpleString.of("news.europe.sport")).getBindings().size());
      assertEquals(1, ad.getBindingsForRoutingAddress(SimpleString.of("news.europe.politics.fr")).getBindings().size());
      assertNull(ad.getBindingsForRoutingAddress(SimpleString.of("news.usa")));
      assertNull(ad.getBindingsForRoutingAddress(SimpleString.of("europe")));
   }

   @Test
   public void testAnyWordsMultipleWildCardsAddressBindingsForRouting() throws Exception {
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), null, null);
      ad.addAddressInfo(new AddressInfo(SimpleString.of("news.#"), RoutingType.MULTICAST));
      ad.addAddressInfo(new AddressInfo(SimpleString.of("news.europe.#"), RoutingType.MULTICAST));
      ad.addBinding(new BindingFake("news.#", "one"));
      ad.addBinding(new BindingFake("news.europe.#", "two"));

      assertEquals(2, ad.getBindingsForRoutingAddress(SimpleString.of("news.europe")).getBindings().size());
      assertEquals(2, ad.getBindingsForRoutingAddress(SimpleString.of("news.europe.sport")).getBindings().size());
      assertEquals(2, ad.getBindingsForRoutingAddress(SimpleString.of("news.europe.politics.fr")).getBindings().size());

      Collection<Binding> bindings = ad.getBindingsForRoutingAddress(SimpleString.of("news.usa")).getBindings();
      assertEquals(1, bindings.size());
      assertEquals("one", bindings.iterator().next().getUniqueName().toString());
      assertNull(ad.getBindingsForRoutingAddress(SimpleString.of("europe")));
   }

   @Test
   public void testNumberOfBindingsThatMatch() throws Exception {

      final WildcardConfiguration configuration = new WildcardConfiguration();
      configuration.setAnyWords('>');
      WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), configuration, null, null);

      ad.addBinding(new BindingFake("T.>", "1"));
      ad.addBinding(new BindingFake("T.>", "2"));
      ad.addBinding(new BindingFake("T.>", "3"));

      assertEquals(3, ad.getBindingsForRoutingAddress(SimpleString.of("T.1")).getBindings().size());
      assertEquals(3, ad.getBindingsForRoutingAddress(SimpleString.of("T.2")).getBindings().size());
      assertEquals(3, ad.getBindingsForRoutingAddress(SimpleString.of("T.3")).getBindings().size());


      assertEquals(3, ad.getExistingBindingsForRoutingAddress(SimpleString.of("T.>")).getBindings().size());

      ad.addBinding(new BindingFake("T.*", "10"));
      assertEquals(1, ad.getExistingBindingsForRoutingAddress(SimpleString.of("T.*")).getBindings().size());

      // wildcard binding should not be added to existing matching wildcards, still 3
      assertEquals(3, ad.getExistingBindingsForRoutingAddress(SimpleString.of("T.>")).getBindings().size());

      assertEquals(4, ad.getBindingsForRoutingAddress(SimpleString.of("T.1")).getBindings().size());
      assertEquals(4, ad.getBindingsForRoutingAddress(SimpleString.of("T.2")).getBindings().size());
      assertEquals(4, ad.getBindingsForRoutingAddress(SimpleString.of("T.3")).getBindings().size());


      ad.addBinding(new BindingFake("T.1.>", "11"));
      assertEquals(1, ad.getExistingBindingsForRoutingAddress(SimpleString.of("T.1.>")).getBindings().size());

      assertEquals(5, ad.getBindingsForRoutingAddress(SimpleString.of("T.1")).getBindings().size());
      assertEquals(4, ad.getBindingsForRoutingAddress(SimpleString.of("T.2")).getBindings().size());
      assertEquals(4, ad.getBindingsForRoutingAddress(SimpleString.of("T.3")).getBindings().size());

      ad.addBinding(new BindingFake("T.1.2", "12"));

      assertEquals(5, ad.getBindingsForRoutingAddress(SimpleString.of("T.1.2")).getBindings().size());

      ad.addBinding(new BindingFake("T.1.2.3.4", "13"));
      assertEquals(5, ad.getBindingsForRoutingAddress(SimpleString.of("T.1.2.3.4")).getBindings().size());

      ad.addBinding(new BindingFake("T.>.4", "14"));

      assertEquals(6, ad.getBindingsForRoutingAddress(SimpleString.of("T.1.2.3.4")).getBindings().size());

      ad.addBinding(new BindingFake("T.1.A.3.4", "15"));

      assertEquals(6, ad.getBindingsForRoutingAddress(SimpleString.of("T.1.A.3.4")).getBindings().size());

   }

   @Test
   public void testConcurrentCalls() throws Exception {
      final WildcardConfiguration configuration = new WildcardConfiguration();
      configuration.setAnyWords('>');
      final WildcardAddressManager ad = new WildcardAddressManager(new BindingFactoryFake(), configuration, null, null);

      final SimpleString wildCard = SimpleString.of("Topic1.>");
      ad.addAddressInfo(new AddressInfo(wildCard, RoutingType.MULTICAST));

      AtomicReference<Throwable> oops = new AtomicReference<>();
      int numSubs = 500;
      int numThreads = 2;
      ExecutorService executorService = Executors.newFixedThreadPool(numThreads);

      for (int i = 0; i < numSubs; i++) {
         final int id = i;

         executorService.submit(() -> {
            try {

               // add/remove is externally sync via postOffice
               synchronized (executorService) {
                  // subscribe as wildcard
                  ad.addBinding(new BindingFake(SimpleString.of("Topic1.>"), SimpleString.of("" + id)));
               }

               SimpleString pubAddr = SimpleString.of("Topic1." + id);
               // publish to new address, will create
               ad.getBindingsForRoutingAddress(pubAddr);

               // publish again, read only
               ad.getBindingsForRoutingAddress(pubAddr);

               ad.getDirectBindings(pubAddr);

            } catch (Exception e) {
               e.printStackTrace();
               oops.set(e);
            }
         });
      }

      executorService.shutdown();
      assertTrue(executorService.awaitTermination(10, TimeUnit.MINUTES), "finished on time");
      assertNull(oops.get(), "no exceptions");
   }


   @Test
   public void testConcurrentCalls2() throws Exception {
      WildcardAddressManager simpleAddressManager = new WildcardAddressManager(new BindingFactoryFake(), new NullStorageManager(), null);

      final int threads = 20;
      final int adds = 1_000;
      final int keep = 100;

      String address = "TheAddress";
      SimpleString addressSimpleString = SimpleString.of(address);

      simpleAddressManager.addAddressInfo(new AddressInfo(address).addRoutingType(RoutingType.MULTICAST));

      ExecutorService executor = Executors.newFixedThreadPool(threads + 1);
      runAfter(executor::shutdownNow);


      CountDownLatch latch = new CountDownLatch(threads);
      CountDownLatch latch2 = new CountDownLatch(1);

      AtomicInteger errors = new AtomicInteger(0);
      AtomicBoolean running = new AtomicBoolean(true);
      runAfter(() -> running.set(false));

      executor.execute(() -> {
         try {
            while (running.get()) {
               // just to make things worse
               simpleAddressManager.getDirectBindings(addressSimpleString);
               Thread.sleep(1);
            }
         } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            errors.incrementAndGet();
         } finally {
            latch2.countDown();
         }
      });

      for (int thread = 0; thread < threads; thread++) {

         final int threadID = thread;

         executor.execute(() -> {
            try {
               for (int add = 0; add < adds; add++) {
                  simpleAddressManager.addBinding(new BindingFake(address, "t" + threadID + "_" + add));
               }

               for (int remove = keep; remove < adds; remove++) {
                  simpleAddressManager.removeBinding(SimpleString.of("t" + threadID + "_" + remove), null);
               }
            } catch (Exception e) {
               logger.warn(e.getMessage(), e);
               errors.incrementAndGet();
            } finally {
               latch.countDown();
            }
         });
      }

      Assertions.assertTrue(latch.await(1, TimeUnit.MINUTES));

      running.set(false);

      Assertions.assertTrue(latch2.await(1, TimeUnit.MINUTES));

      Assertions.assertEquals(0, errors.get());

      Collection<Binding> bindings = simpleAddressManager.getDirectBindings(SimpleString.of(address));

      Set<String> result = new HashSet<>();

      bindings.forEach(b -> result.add(b.getUniqueName().toString()));

      Assertions.assertEquals(threads * keep, result.size());

      for (int thread = 0; thread < threads; thread++) {
         for (int add = 0; add < keep; add++) {
            Assertions.assertTrue(result.contains("t" + thread + "_" + add));
         }
      }
   }

   static class BindingFactoryFake implements BindingsFactory {

      @Override
      public boolean isAddressBound(SimpleString address) throws Exception {
         return false;
      }

      @Override
      public Bindings createBindings(SimpleString address) {
         return new BindingsFake(address);
      }
   }

   static class BindingFake implements Binding {

      final SimpleString address;
      final SimpleString id;

      BindingFake(String addressParameter, String id) {
         this(SimpleString.of(addressParameter), SimpleString.of(id));
      }

      BindingFake(SimpleString addressParameter, SimpleString id) {
         this.address = addressParameter;
         this.id = id;
      }

      @Override
      public void unproposed(SimpleString groupID) {

      }

      @Override
      public SimpleString getAddress() {
         return address;
      }

      @Override
      public Bindable getBindable() {
         return null;
      }

      @Override
      public BindingType getType() {
         return null;
      }

      @Override
      public SimpleString getUniqueName() {
         return id;
      }

      @Override
      public SimpleString getRoutingName() {
         return null;
      }

      @Override
      public SimpleString getClusterName() {
         return null;
      }

      @Override
      public Filter getFilter() {
         return null;
      }

      @Override
      public boolean isHighAcceptPriority(Message message) {
         return false;
      }

      @Override
      public boolean isExclusive() {
         return false;
      }

      @Override
      public Long getID() {
         return 0L;
      }

      @Override
      public int getDistance() {
         return 0;
      }

      @Override
      public void route(Message message, RoutingContext context) throws Exception {
      }

      @Override
      public void close() throws Exception {
      }

      @Override
      public String toManagementString() {
         return "FakeBiding Address=" + this.address;
      }

      @Override
      public boolean isConnected() {
         return true;
      }

      @Override
      public void routeWithAck(Message message, RoutingContext context) {

      }
   }

   static class BindingsFake implements Bindings {

      SimpleString name;
      ConcurrentMap<String, Binding> bindings = new ConcurrentHashMap<>();

      BindingsFake(SimpleString address) {
         this.name = address;
      }

      @Override
      public boolean hasLocalBinding() {
         return false;
      }

      @Override
      public Collection<Binding> getBindings() {
         return bindings.values();
      }

      @Override
      public void addBinding(Binding binding) {
         bindings.put(String.valueOf(binding.getUniqueName()), binding);
      }

      @Override
      public Binding removeBindingByUniqueName(SimpleString uniqueName) {
         return bindings.remove(String.valueOf(uniqueName));
      }

      @Override
      public SimpleString getName() {
         return name;
      }

      @Override
      public void setMessageLoadBalancingType(MessageLoadBalancingType messageLoadBalancingType) {

      }

      @Override
      public Binding getBinding(String name) {
         return bindings.get(name);
      }

      @Override
      public void forEach(BiConsumer<String, Binding> bindingConsumer) {
         bindings.forEach(bindingConsumer);
      }

      @Override
      public int size() {
         return bindings.size();
      }

      @Override
      public MessageLoadBalancingType getMessageLoadBalancingType() {
         return null;
      }

      @Override
      public void unproposed(SimpleString groupID) {
      }

      @Override
      public void updated(QueueBinding binding) {
      }

      @Override
      public Message redistribute(Message message,
                                  Queue originatingQueue,
                                  RoutingContext context) throws Exception {
         return null;
      }

      @Override
      public void route(Message message, RoutingContext context) throws Exception {
         logger.debug("routing message: {}", message);
      }

      @Override
      public boolean allowRedistribute() {
         return false;
      }
   }

}
