/*
 * 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;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.ServerSocket;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.apache.activemq.artemiswrapper.ArtemisBrokerHelper;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.transport.tcp.TcpTransportFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Poor mans way of getting JUnit to run a test case through a few different combinations of options. Usage: If you have
 * a test case called testFoo what you want to run through a few combinations, of of values for the attributes age and
 * color, you would something like:
 * <pre>{@code
 * public void initCombosForTestFoo() {
 *    addCombinationValues( "age", new Object[]{ new Integer(21), new Integer(30) } );
 *    addCombinationValues( "color", new Object[]{"blue", "green"} );
 * }
 * }</pre>
 * The testFoo test case would be run for each possible combination of age and color that you setup in the
 * initCombosForTestFoo method. Before each combination is run, the age and color fields of the test class are set to
 * one of the values defined. This is done before the normal setUp method is called. If you want the test combinations
 * to show up as separate test runs in the JUnit reports, add a suite method to your test case similar to:
 * <pre>{@code
 * public static Test suite() {
 *    return suite(FooTest.class);
 * }
 * }</pre>
 */
public abstract class CombinationTestSupport extends AutoFailTestSupport {

   private static final Logger LOG = LoggerFactory.getLogger(CombinationTestSupport.class);

   private final Map<String, ComboOption> comboOptions = new HashMap<>();
   private boolean combosEvaluated;
   private Map<String, Object> options;
   protected File basedir;

   static protected File basedir(Class<?> clazz) {
      try {
         ProtectionDomain protectionDomain = clazz.getProtectionDomain();
         return new File(new File(protectionDomain.getCodeSource().getLocation().getPath()), "../..").getCanonicalFile();
      } catch (IOException e) {
         return new File(".");
      }
   }

   static class ComboOption {

      final String attribute;
      final LinkedHashSet<Object> values = new LinkedHashSet<>();

      public ComboOption(String attribute, Collection<Object> options) {
         this.attribute = attribute;
         this.values.addAll(options);
      }
   }

   public CombinationTestSupport() {
      basedir = basedir(getClass());
   }

   public void addCombinationValues(String attribute, Object[] options) {
      ComboOption co = this.comboOptions.get(attribute);
      if (co == null) {
         this.comboOptions.put(attribute, new ComboOption(attribute, Arrays.asList(options)));
      } else {
         co.values.addAll(Arrays.asList(options));
      }
   }

   @Override
   protected void tearDown() throws Exception {
      super.tearDown();
      checkStopped();
   }

   public static void checkStopped() throws Exception {
      ArtemisBrokerHelper.stopArtemisBroker();
      boolean notStopped = BrokerService.checkStopped();
      TcpTransportFactory.clearService();
      if (notStopped) {
         fail("brokers not stopped see exceptions above");
      }
      ServerSocket socket = null;
      try {
         socket = new ServerSocket(61616);
      } catch (IOException e) {
         fail("61616 port not released");
      } finally {
         if (socket != null)
            try {
               socket.close();
            } catch (IOException e) {
            }
      }
   }

   @Override
   public void runBare() throws Throwable {
      if (combosEvaluated) {
         super.runBare();
      } else {
         CombinationTestSupport[] combinations = getCombinations();
         for (int i = 0; i < combinations.length; i++) {
            CombinationTestSupport test = combinations[i];
            if (getName() == null || getName().equals(test.getName())) {
               test.runBare();
            }
         }
      }
   }

   private void setOptions(Map<String, Object> options) throws NoSuchFieldException, IllegalAccessException {
      this.options = options;
      for (Iterator<String> iterator = options.keySet().iterator(); iterator.hasNext(); ) {
         String attribute = iterator.next();
         Object value = options.get(attribute);
         try {
            Field field = getClass().getField(attribute);
            field.set(this, value);
         } catch (Throwable e) {
            try {
               boolean found = false;
               String setterName = "set" + attribute.substring(0, 1).toUpperCase() +
                  attribute.substring(1);
               for (Method method : getClass().getMethods()) {
                  if (method.getName().equals(setterName)) {
                     method.invoke(this, value);
                     found = true;
                     break;
                  }
               }

               if (!found) {
                  throw new NoSuchMethodError("No setter found for field: " + attribute);
               }

            } catch (Throwable ex) {
               LOG.info("Could not set field '" + attribute + "' to value '" + value +
                           "', make sure the field exists and is public or has a setter.");
            }
         }
      }
   }

   private CombinationTestSupport[] getCombinations() {
      try {
         Method method = getClass().getMethod("initCombos", (Class[]) null);
         method.invoke(this, (Object[]) null);
      } catch (Throwable e) {
      }

      String name = getName().split(" ")[0];
      String comboSetupMethodName = "initCombosFor" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
      try {
         Method method = getClass().getMethod(comboSetupMethodName, (Class[]) null);
         method.invoke(this, (Object[]) null);
      } catch (Throwable e) {
      }

      try {
         List<Map<String, Object>> expandedOptions = new ArrayList<>();
         expandCombinations(new ArrayList<>(comboOptions.values()), expandedOptions);

         if (expandedOptions.isEmpty()) {
            combosEvaluated = true;
            return new CombinationTestSupport[]{this};
         } else {

            List<CombinationTestSupport> result = new ArrayList<>();
            // Run the test case for each possible combination
            for (Iterator<Map<String, Object>> iter = expandedOptions.iterator(); iter.hasNext(); ) {
               CombinationTestSupport combo = (CombinationTestSupport) TestSuite.createTest(getClass(), name);
               combo.combosEvaluated = true;
               combo.setOptions(iter.next());
               result.add(combo);
            }

            CombinationTestSupport rc[] = new CombinationTestSupport[result.size()];
            result.toArray(rc);
            return rc;
         }
      } catch (Throwable e) {
         combosEvaluated = true;
         return new CombinationTestSupport[]{this};
      }

   }

   private void expandCombinations(List<ComboOption> optionsLeft, List<Map<String, Object>> expandedCombos) {
      if (!optionsLeft.isEmpty()) {
         Map<String, Object> map;
         if (comboOptions.size() == optionsLeft.size()) {
            map = new HashMap<>();
            expandedCombos.add(map);
         } else {
            map = expandedCombos.get(expandedCombos.size() - 1);
         }

         LinkedList<ComboOption> l = new LinkedList<>(optionsLeft);
         ComboOption comboOption = l.removeLast();
         int i = 0;
         if (comboOption.values.isEmpty() && !l.isEmpty()) {
            expandCombinations(l, expandedCombos);
         } else {
            for (Iterator<Object> iter = comboOption.values.iterator(); iter.hasNext(); ) {
               Object value = iter.next();
               if (i != 0) {
                  map = new HashMap<>(map);
                  expandedCombos.add(map);
               }
               map.put(comboOption.attribute, value);
               expandCombinations(l, expandedCombos);
               i++;
            }
         }
      }
   }

   public static Test suite(Class<? extends CombinationTestSupport> clazz) {
      TestSuite suite = new TestSuite();

      List<String> names = new ArrayList<>();
      Method[] methods = clazz.getMethods();
      for (int i = 0; i < methods.length; i++) {
         String name = methods[i].getName();
         if (names.contains(name) || !isPublicTestMethod(methods[i])) {
            continue;
         }
         names.add(name);
         Test test = TestSuite.createTest(clazz, name);
         if (test instanceof CombinationTestSupport) {
            CombinationTestSupport[] combinations = ((CombinationTestSupport) test).getCombinations();
            for (int j = 0; j < combinations.length; j++) {
               suite.addTest(combinations[j]);
            }
         } else {
            suite.addTest(test);
         }
      }
      return suite;
   }

   private static boolean isPublicTestMethod(Method m) {
      return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
   }

   private static boolean isTestMethod(Method m) {
      String name = m.getName();
      Class<?>[] parameters = m.getParameterTypes();
      Class<?> returnType = m.getReturnType();
      return parameters.length == 0 && name.startsWith("test") && returnType.equals(Void.TYPE);
   }

   @Override
   public String getName() {
      return getName(false);
   }

   public String getName(boolean original) {
      if (options != null && !original) {
         return super.getName() + " " + options;
      }
      return super.getName();
   }
}
