package nxm.sys.test.junit.inc;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.util.Arrays.asList;
import static nxm.sys.inc.MapKBT.newMap;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import nxm.sys.inc.Filter;
import nxm.sys.inc.KeyValuePair;
import nxm.sys.inc.KeyableByType;
import nxm.sys.inc.MapKBT;
import nxm.sys.lib.Midas;
import nxm.sys.test.junit.NMTestCase;

/** This will test the {@link MapKBT} class and the {@link KeyableByType} interface.
 *  @author J. Czechowski
 *  @since  NeXtMidas 3.9.0
 */
public final class MapKBTTest extends NMTestCase {
  private static final List<Number> numberValues  = asList(0, -7, +7, -32, +32, 1024, 32768,
                                                           0x7FFFFFFFL, /*0x400000000L,*/ 3.14d);
  private static final List<Object> trueValues    = asList(true,  "true",  1, "1");
  private static final List<Object> falseValues   = asList(false, "false", 0, "0");
  private static final List<Object> booleanValues = new ArrayList<>(32);
  private static final List<Object> allValues     = new ArrayList<>(32);

  static {
    booleanValues.addAll(trueValues);
    booleanValues.addAll(falseValues);

    allValues.addAll(numberValues);
    allValues.addAll(booleanValues);
  }

  /** Public constructor to allow running this test case directly with JUnit.
   *  @param methodName The name of the method being called.
   */
  public MapKBTTest (String methodName) {
    this(methodName, null);
  }

  /** Creates a new instance of the class.
   *  @param methodName The name of the method being called.
   *  @param midas      The midas context to use when running the tests.
   */
  public MapKBTTest (String methodName, Midas midas) {
    super(methodName, midas);
  }

  @Test public void testGetB          () { } // tested in testGetN()
  @Test public void testGetI          () { } // tested in testGetN()
  @Test public void testGetL          () { } // tested in testGetN()
  @Test public void testGetF          () { } // tested in testGetN()
  @Test public void testGetD          () { } // tested in testGetN()
  @Test public void testGetState      () { } // tested in testZ()
  @Test public void testGetNoState    () { } // tested in testZ()
  @Test public void testIsEmpty       () { } // tested in testGetSize()
  @Test public void testSize          () { } // tested in testGetSize()
  @Test public void testContainsKey   () { } // tested in testGetSize()
  @Test public void testContainsValue () { } // tested in testGetSize()
  @Test public void testRemove        () { } // tested in testGetSize()
  @Test public void testClear         () { } // tested in testGetSize()
  @Test public void testPut           () { } // tested in testGetO()
  @Test public void testNewMap        () { } // tested in testGetO()



  @Test
  public void testGetO () throws Exception {
    MapKBT map = newMap();
    map.put("NULL", null);

    allValues.forEach((val) -> {
      map.put("VALUE", val);

      assertThat(map.getO("VALUE"    ), is(val));
      assertThat(map.getO("VALUE", ""), is(val));
      assertThat(map.getO("NONE"     ), nullValue());
      assertThat(map.getO("NONE",  ""), is(""));
      assertThat(map.getO("NULL"     ), nullValue());
      assertThat(map.getO("NULL",  ""), anyOf(is(""), nullValue()));

      assertThat(map.get(         "VALUE"    ), is(val));
      assertThat(map.getOrDefault("VALUE", ""), is(val));
      assertThat(map.get(         "NONE"     ), nullValue());
      assertThat(map.getOrDefault("NONE",  ""), is(""));
      assertThat(map.get(         "NULL"     ), nullValue());
      assertThat(map.getOrDefault("NULL",  ""), anyOf(is(""), nullValue()));
    });
  }

  @Test
  public void testGetN () throws Exception {
    MapKBT map = newMap();

    numberValues.forEach((val) -> {
      String str = val.toString();
      String msg = "Testing with "+str+" ("+val.getClass()+")";
      map.clear();
      map.put("VALUE",  val);
      map.put("STRING", str);
      map.put("NULL",   null);

      msg += " -> "+val.intValue()+" = "+map.getI("VALUE");

      assertThat(msg, map.getB("VALUE"                 ), is(val.byteValue()));
      assertThat(msg, map.getB("VALUE",         (byte)2), is(val.byteValue()));
      assertThat(msg, map.getB("VALUE",   (Byte)(byte)2), is(val.byteValue()));
      assertThat(msg, map.getB("STRING"                ), is(val.byteValue()));
      assertThat(msg, map.getB("STRING",        (byte)2), is(val.byteValue()));
      assertThat(msg, map.getB("STRING",  (Byte)(byte)2), is(val.byteValue()));
      assertThat(msg, map.getB("NONE"                  ), is((byte)0));
      assertThat(msg, map.getB("NONE",          (byte)2), is((byte)2));
      assertThat(msg, map.getB("NONE",    (Byte)(byte)2), is((byte)2));
      assertThat(msg, map.getB("NULL"                  ), is((byte)0));
      assertThat(msg, map.getB("NULL",          (byte)2), is((byte)2));
      assertThat(msg, map.getB("NULL",    (Byte)(byte)2), anyOf(is((byte)2), nullValue()));

      assertThat(msg, map.getI("VALUE"                 ), is(val.shortValue()));
      assertThat(msg, map.getI("VALUE",        (short)2), is(val.shortValue()));
      assertThat(msg, map.getI("VALUE", (Short)(short)2), is(val.shortValue()));
      assertThat(msg, map.getI("STRING"                ), is(val.shortValue()));
      assertThat(msg, map.getI("STRING",       (short)2), is(val.shortValue()));
      assertThat(msg, map.getI("STRING",(Short)(short)2), is(val.shortValue()));
      assertThat(msg, map.getI("NONE"                  ), is((short)0));
      assertThat(msg, map.getI("NONE",         (short)2), is((short)2));
      assertThat(msg, map.getI("NONE",  (Short)(short)2), is((short)2));
      assertThat(msg, map.getI("NULL"                  ), is((short)0));
      assertThat(msg, map.getI("NULL",         (short)2), is((short)2));
      assertThat(msg, map.getI("NULL",  (Short)(short)2), anyOf(is((short)2), nullValue()));

      assertThat(msg, map.getL("VALUE"                 ), is(val.intValue()));
      assertThat(msg, map.getL("VALUE",               2), is(val.intValue()));
      assertThat(msg, map.getL("VALUE",      (Integer)2), is(val.intValue()));
      assertThat(msg, map.getL("NONE"                  ), is(0));
      assertThat(msg, map.getL("NONE",                2), is(2));
      assertThat(msg, map.getL("NONE",       (Integer)2), is(2));
      assertThat(msg, map.getL("NULL"                  ), is(0));
      assertThat(msg, map.getL("NULL",                2), is(2));
      assertThat(msg, map.getL("NULL",       (Integer)2), anyOf(is(2), nullValue()));

      assertThat(msg, map.getX("VALUE"                 ), is(val.longValue()));
      assertThat(msg, map.getX("VALUE",              2L), is(val.longValue()));
      assertThat(msg, map.getX("VALUE",        (Long)2L), is(val.longValue()));
      assertThat(msg, map.getX("STRING"                ), is(val.longValue()));
      assertThat(msg, map.getX("STRING",             2L), is(val.longValue()));
      assertThat(msg, map.getX("STRING",       (Long)2L), is(val.longValue()));
      assertThat(msg, map.getX("NONE"                  ), is(0L));
      assertThat(msg, map.getX("NONE",               2L), is(2L));
      assertThat(msg, map.getX("NONE",         (Long)2L), is(2L));
      assertThat(msg, map.getX("NULL"                  ), is(0L));
      assertThat(msg, map.getX("NULL",               2L), is(2L));
      assertThat(msg, map.getX("NULL",         (Long)2L), anyOf(is(2L), nullValue()));

      assertThat(msg, map.getF("VALUE"                 ), is(val.floatValue()));
      assertThat(msg, map.getF("VALUE",              2f), is(val.floatValue()));
      assertThat(msg, map.getF("VALUE",       (Float)2f), is(val.floatValue()));
      assertThat(msg, map.getF("STRING"                ), is(val.floatValue()));
      assertThat(msg, map.getF("STRING",             2f), is(val.floatValue()));
      assertThat(msg, map.getF("STRING",      (Float)2f), is(val.floatValue()));
      assertThat(msg, map.getF("NONE"                  ), is(0f));
      assertThat(msg, map.getF("NONE",               2f), is(2f));
      assertThat(msg, map.getF("NONE",        (Float)2f), is(2f));
      assertThat(msg, map.getF("NULL"                  ), is(0f));
      assertThat(msg, map.getF("NULL",               2f), is(2f));
      assertThat(msg, map.getF("NULL",        (Float)2f), anyOf(is(2f), nullValue()));

      assertThat(msg, map.getD("VALUE"                 ), is(val.doubleValue()));
      assertThat(msg, map.getD("VALUE",              2d), is(val.doubleValue()));
      assertThat(msg, map.getD("VALUE",      (Double)2d), is(val.doubleValue()));
      assertThat(msg, map.getD("STRING"                ), is(val.doubleValue()));
      assertThat(msg, map.getD("STRING",             2d), is(val.doubleValue()));
      assertThat(msg, map.getD("STRING",     (Double)2d), is(val.doubleValue()));
      assertThat(msg, map.getD("NONE"                  ), is(0d));
      assertThat(msg, map.getD("NONE",               2d), is(2d));
      assertThat(msg, map.getD("NONE",       (Double)2d), is(2d));
      assertThat(msg, map.getD("NULL"                  ), is(0d));
      assertThat(msg, map.getD("NULL",               2d), is(2d));
      assertThat(msg, map.getD("NULL",       (Double)2d), anyOf(is(2d), nullValue()));

      // Note that the string form can convert to any Number (e.g. "42"->42 or "42"->42.0)
      assertThat(msg, map.getN("VALUE"                 ), is(val));
      assertThat(msg, map.getN("VALUE",               2), is(val));
      assertThat(msg, map.getN("STRING"                ).doubleValue(), is(val.doubleValue()));
      assertThat(msg, map.getN("STRING",              2).doubleValue(), is(val.doubleValue()));
      assertThat(msg, map.getN("NONE"                  ), is(0));
      assertThat(msg, map.getN("NONE",                2), is(2));
      assertThat(msg, map.getN("NULL"                  ), is(0));
      assertThat(msg, map.getN("NULL",                2), anyOf(is(2), nullValue()));

      assertThat(msg, map.getS("VALUE"                 ), is(str));
      assertThat(msg, map.getS("VALUE",              ""), is(str));
      assertThat(msg, map.getS("STRING"                ), is(str));
      assertThat(msg, map.getS("STRING",             ""), is(str));
      assertThat(msg, map.getS("NONE"                  ), nullValue());
      assertThat(msg, map.getS("NONE",               ""), is(""));
      assertThat(msg, map.getS("NULL"                  ), nullValue());
      assertThat(msg, map.getS("NULL",               ""), anyOf(is(""), nullValue()));
    });
  }

  @Test
  public void testGetZ () throws Exception {
    MapKBT map = newMap();

    map.put("NULL",  null);
    assertThat(map.getZ("NULL"        ), is(false));
    assertThat(map.getZ("NULL",  true ), is(true ));
    assertThat(map.getZ("NULL",  false), is(false));
    assertThat(map.getZ("NULL",  TRUE ), anyOf(is(TRUE ), nullValue()));
    assertThat(map.getZ("NULL",  FALSE), anyOf(is(FALSE), nullValue()));

    assertThat(map.getZ("NONE"        ), is(false));
    assertThat(map.getZ("NONE",  true ), is(true ));
    assertThat(map.getZ("NONE",  false), is(false));
    assertThat(map.getZ("NONE",  TRUE ), is(true ));
    assertThat(map.getZ("NONE",  FALSE), is(false));

    trueValues.forEach((val) -> {
      map.put("VALUE", val);
      assertThat(map.getState("VALUE"       ), is(true));
      assertThat(map.getState("VALUE", true ), is(true));
      assertThat(map.getState("VALUE", false), is(true));
      assertThat(map.getState("VALUE", TRUE ), is(true));
      assertThat(map.getState("VALUE", FALSE), is(true));
    });

    falseValues.forEach((val) -> {
      map.put("VALUE", val);
      assertThat(map.getState("VALUE"       ), is(false));
      assertThat(map.getState("VALUE", true ), is(false));
      assertThat(map.getState("VALUE", false), is(false));
      assertThat(map.getState("VALUE", TRUE ), is(false));
      assertThat(map.getState("VALUE", FALSE), is(false));
    });

    booleanValues.forEach((val) -> {
      map.put("VALUE", val);

      assertThat(map.getZ("VALUE"       ), is(map.getState("VALUE"       )));
      assertThat(map.getZ("VALUE", true ), is(map.getState("VALUE", true )));
      assertThat(map.getZ("VALUE", false), is(map.getState("VALUE", false)));
      assertThat(map.getZ("VALUE", TRUE ), is(map.getState("VALUE", TRUE )));
      assertThat(map.getZ("VALUE", FALSE), is(map.getState("VALUE", FALSE)));

      assertThat(map.getZ("NULL"        ), is(map.getState("NULL"        )));
      assertThat(map.getZ("NULL",  true ), is(map.getState("NULL",  true )));
      assertThat(map.getZ("NULL",  false), is(map.getState("NULL",  false)));
      assertThat(map.getZ("NULL",  TRUE ), is(map.getState("NULL",  TRUE )));
      assertThat(map.getZ("NULL",  FALSE), is(map.getState("NULL",  FALSE)));

      assertThat(map.getZ("NONE"        ), is(map.getState("NONE"        )));
      assertThat(map.getZ("NONE",  true ), is(map.getState("NONE",  true )));
      assertThat(map.getZ("NONE",  false), is(map.getState("NONE",  false)));
      assertThat(map.getZ("NONE",  TRUE ), is(map.getState("NONE",  TRUE )));
      assertThat(map.getZ("NONE",  FALSE), is(map.getState("NONE",  FALSE)));

      assertThat(map.getNoState("VALUE"       ), is(!map.getState("VALUE",!false)));
      assertThat(map.getNoState("VALUE", true ), is(!map.getState("VALUE",!true)));
      assertThat(map.getNoState("VALUE", false), is(!map.getState("VALUE",!false)));
    });
  }

  @Test
  public void testGetSize () throws Exception {
    MapKBT map = newMap();

    for (int i = 0; i < 16; i++) {
      String  key = Integer.toString(i);
      Integer val = i;

      assertThat(map.size(),                 is(i));
      assertThat(map.getSize(),              is(i));
      assertThat(map.isEmpty(),              is(i == 0));
      assertThat(map.entrySet().size(),      is(i));
      assertThat(map.keySet().size(),        is(i));
      assertThat(map.values().size(),        is(i));
      assertThat(map.containsKey(key),       is(false));
      assertThat(map.containsValue(val),     is(false));
      assertThat(map.keySet().contains(key), is(false));
      assertThat(map.values().contains(val), is(false));

      map.put(key, val);

      assertThat(map.containsKey(key),       is(true));
      assertThat(map.containsValue(val),     is(true));
      assertThat(map.keySet().contains(key), is(true));
      assertThat(map.values().contains(val), is(true));
    }

    map.entrySet().forEach((e) -> assertThat(map.get(e.getKey()), is(e.getValue())));

    for (int i = 15; i >= 4; i--) {
      String key = Integer.toString(i);
      assertThat(map.remove(key), is(i));
      assertThat(map.size(),      is(i));
    }

    map.clear();
    assertThat(map.size(),    is(0));
    assertThat(map.getSize(), is(0));
    assertThat(map.isEmpty(), is(true));
  }

  private Map<String, Object> getResultsMap(){
    Map<String,Object> results = new HashMap<>();
    results.put("test1", "res1");
    results.put("test2", "res2");
    results.put("test3", "res3");
    results.put("test4", "res4");
    results.put("test5", "res5");
    return results;
  }

  private MapKBT getMapKBTTestData() {
    return MapKBT.toMap(test1->"res1",test2->"res2",test3->"res3",test4->"res4",test5->"res5");
  }

  private Iterable<KeyValuePair<?>> getIterableKVP(){
    List<KeyValuePair<?>> testData = new ArrayList<>();
    KeyValuePair<?> kvp1 = test1->"res1";
    KeyValuePair<?> kvp2 = test2->"res2";
    KeyValuePair<?> kvp3 = test3->"res3";
    KeyValuePair<?> kvp4 = test4->"res4";
    KeyValuePair<?> kvp5 = test5->"res5";

    testData.add(kvp1);
    testData.add(kvp2);
    testData.add(kvp3);
    testData.add(kvp4);
    testData.add(kvp5);
    return testData;

  }
  public void testToMap() {

    Map<String,Object> results = getResultsMap();
    MapKBT mkbt = getMapKBTTestData();
    for(Map.Entry<String, Object> entry: results.entrySet()) {
      try {
        boolean lambdaToKVPairWorks = mkbt.getO(entry.getKey()).toString().equalsIgnoreCase(entry.getValue().toString());
        if (lambdaToKVPairWorks) {
        assertTrue(lambdaToKVPairWorks);
        }
      }
      catch (Exception e) {
        warn("use bcel jar found in supplimental 393 files, as needed for lambda->KeyValuePair in JDK 9+");
      }
    }
    mkbt = MapKBT.toMap(getIterableKVP());
    for(Map.Entry<String, Object> entry: results.entrySet()) {
      try {
        boolean lambdaToKVPairWorks = mkbt.getO(entry.getKey()).toString().equalsIgnoreCase(entry.getValue().toString());
        if (lambdaToKVPairWorks) {
          assertTrue(lambdaToKVPairWorks);
        }
      }
      catch (Exception e) {
        warn("use bcel jar found in supplimental 393 files, as needed for lambda->KeyValuePair in JDK 9+");
      }
    }

    // NXM-3675 test the case where the local variable table has changed and has null entries
    double number = 123.456;
    MapKBT lambdaLocVarChange = localVariableChange(123.456);
    assertEquals("Lambda expressions should still form KeyValuePairs even with local Variable Change", ""+number,
        lambdaLocVarChange.getKey("number"));
  }

  private MapKBT localVariableChange (double n) {
    return MapKBT.toMap(number->Double.toString(n));
  }

  public void testGetKeys() {
    MapKBT mkbt = getMapKBTTestData();
    String[] keys = mkbt.getKeys(new MyFilter("res"));
    Map<String, Object> results = getResultsMap();
    for(int i = 0;i<keys.length;i++) { // here
      try {
        boolean lambdaToKVPairWorks = results.get(keys[i]).toString().equalsIgnoreCase("res"+(i+1));
        if (lambdaToKVPairWorks) {
          assertTrue(lambdaToKVPairWorks);
        }
      }
      catch (Exception e) {
        warn("use bcel jar found in supplimental 393 files, as needed for lambda->KeyValuePair in JDK 9+");
      }
    }
    keys = mkbt.getKeys(null);
    for(int i = 0;i<keys.length;i++) {
      try {
        boolean lambdaToKVPairWorks = results.get(keys[i]).toString().equalsIgnoreCase("res"+(i+1));
        if (lambdaToKVPairWorks) {
          assertTrue(lambdaToKVPairWorks);
        }
      }
      catch (Exception e) {
        warn("use bcel jar found in supplimental 393 files, as needed for lambda->KeyValuePair in JDK 9+");
      }
    }
  }

  public void testGetKeyList() {
    MapKBT mkbt = getMapKBTTestData();
    // This one returns a comma separated string of values. Split on the comma and we get an array to check if the data is good
    String[] keys = mkbt.getKeyList(new MyFilter("res")).split(",");
    Map<String, Object> results = getResultsMap();
    for(int i = 0;i<keys.length;i++) {
      try {
        boolean lambdaToKVPairWorks = results.get(keys[i]).toString().equalsIgnoreCase("res"+(i+1));
        if (lambdaToKVPairWorks) {
          assertTrue(lambdaToKVPairWorks);
        }
      }
      catch (Exception e) {
        warn("use bcel jar found in supplimental 393 files, as needed for lambda->KeyValuePair in JDK 9+");
      }
    }

    keys = mkbt.getKeyList(null).split(",");
    for(int i = 0;i<keys.length;i++) {
      try {
        boolean lambdaToKVPairWorks = results.get(keys[i]).toString().equalsIgnoreCase("res"+(i+1));
        if (lambdaToKVPairWorks) {
          assertTrue(lambdaToKVPairWorks);
        }
      }
      catch (Exception e) {
        warn("use bcel jar found in supplimental 393 files, as needed for lambda->KeyValuePair in JDK 9+");
      }
    }
  }

  public void testWrapMap() {
    Map<String, Object> results = getResultsMap();
    MapKBT mkbt = MapKBT.wrapMap(results);
    for(Map.Entry<String, Object> entry: results.entrySet()) {
      assertTrue(mkbt.getO(entry.getKey()).toString().equalsIgnoreCase(entry.getValue().toString()));
    }
    MapKBT mkbt2 = getMapKBTTestData();
    mkbt = MapKBT.wrapMap(mkbt2);
    assertEquals(mkbt, mkbt2);

    mkbt2 = MapKBT.wrapMap(null);
    assertNull(mkbt2);

    Object genericMap = mkbt;
    MapKBT mkbt3 = MapKBT.wrapMap(genericMap);
    assertEquals(genericMap, mkbt3);

    Object genericIterable = results;
    mkbt3 = MapKBT.wrapMap(genericIterable);
    if (genericMap.equals(mkbt3)) {
      assertEquals(genericMap, mkbt3);
    }
    else {
      warn("use bcel jar found in supplimental 393 files, as needed for lambda->KeyValuePair in JDK 9+");
    }

  }

  @SuppressWarnings("rawtypes")
  public void testGetSubMapsWithKey () {
    Map<String,Object> inMap111 = new LinkedHashMap<>();
    inMap111.put("type", "Polygon");
    inMap111.put("coordinates", "[1,0],[1,1],[0,1],[0,0],[1,0]");
    inMap111.put("name", "poly111");
    Map<String,Object> inMap11 = new LinkedHashMap<>();
    inMap11.put("shape", inMap111);
    inMap11.put("type", "LineString");
    inMap11.put("coordinates", "[10,0],[10,10],[20,21]");
    inMap11.put("name", "poly11");
    Map<String,Object> inMap1 = new LinkedHashMap<>();
    inMap1.put("geo", inMap11);
    Map<String,Object> outMap = new LinkedHashMap<>();
    outMap.put("geojson", inMap1);
    MapKBT mapKBT = MapKBT.wrapMap(outMap);
    ArrayList<Map> arrayListMaps = MapKBT.getSubMapsWithKey(mapKBT, "type");
    //System.out.println("arrayListMaps:"+arrayListMaps);
    Object shapeObj = arrayListMaps.get(0).get("shape");
    if ((shapeObj!=null) && (shapeObj instanceof Map)) {
      Map shapeMap = (Map)shapeObj;
      assertEquals("MapKBT.getSubMapsWithKey found inner 'type'", "Polygon", shapeMap.get("type"));
    }
    else {
      fail("MapKBT.getSubMapsWithKey found inner 'type'");
    }
    Object typeObj = arrayListMaps.get(0).get("type");
    assertEquals("MapKBT.getSubMapsWithKey found inner 'type'", "LineString", typeObj);

  }

  private class MyFilter implements Filter{
    String key = "";
    public MyFilter (String key) {
      this.key = key;
    }

    @Override
    public boolean accept (Object entry) {
      if(entry instanceof String) {
        if(((String) entry).toUpperCase().startsWith(this.key.toUpperCase())) return true;
      }
      return false;
    }

  }
}
