package nxm.sys.test.junit.lib;

import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;

import nxm.sys.lib.Data;
import nxm.sys.lib.Midas;
import nxm.sys.lib.PredicateType;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.test.junit.NMTestCase;

/** This will test the PredicateType class.
    @author  Jeanette McCall
    @since NeXtMidas 4.1.1
 */
public final class PredicateTypeTest extends NMTestCase {
  private boolean printPasses = false;
  private boolean useAsserts = true;

  /** public constructor to allow running this test case directly with JUnit. */
  public PredicateTypeTest (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 testTo use when running the tests.
   */
  public PredicateTypeTest (String methodName, Midas midas) {
    super(methodName, midas);
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////

  public void testAddKeyFunctionString() {
    //*****************************************************************
    // Create Predicates and Predicate Types and add them to the Map
    //*****************************************************************
    Map<String,PredicateType> mapPT = PredicateType.getNewPredicateTypeMap();

    // Normal IF tests
    PredicateType.addKeyFunction(mapPT,"KEY_EQ","EQ","100");
    PredicateType pt_eq = mapPT.get("KEY_EQ");

    PredicateType.addKeyFunction(mapPT,"KEY_NE","NE 100");
    PredicateType pt_ne = mapPT.get("KEY_NE");

    PredicateType.addKeyFunction(mapPT,"KEY_LT","LT 100");
    PredicateType pt_lt = mapPT.get("KEY_LT");

    PredicateType.addKeyFunction(mapPT,"KEY_LE","LE 100");
    PredicateType pt_le = mapPT.get("KEY_LE");

    PredicateType.addKeyFunction(mapPT,"KEY_GT","GT 100");
    PredicateType pt_gt = mapPT.get("KEY_GT");

    PredicateType.addKeyFunction(mapPT,"KEY_GE","GE 100");
    PredicateType pt_ge = mapPT.get("KEY_GE");

    PredicateType.addKeyFunction(mapPT,"KEY_EQT","EQT 100");
    PredicateType pt_eqt = mapPT.get("KEY_EQT");

    PredicateType.addKeyFunction(mapPT,"KEY_NEQT","NEQT 100");
    PredicateType pt_nEqt = mapPT.get("KEY_NEQT");

    PredicateType.addKeyFunction(mapPT,"KEY_EQS","EQS hello");
    PredicateType pt_eqs = mapPT.get("KEY_EQS");

    PredicateType.addKeyFunction(mapPT,"KEY_NEQS","NEQS hello");
    PredicateType pt_nEqs = mapPT.get("KEY_NEQS");

    PredicateType.addKeyFunction(mapPT,"KEY_STARTS","STARTS he");
    PredicateType pt_starts = mapPT.get("KEY_STARTS");

    PredicateType.addKeyFunction(mapPT,"KEY_NSTARTS","NSTARTS he");
    PredicateType pt_nStarts = mapPT.get("KEY_NSTARTS");

    PredicateType.addKeyFunction(mapPT,"KEY_ENDS","ENDS lo");
    PredicateType pt_ends = mapPT.get("KEY_ENDS");

    PredicateType.addKeyFunction(mapPT,"KEY_NENDS","NENDS lo");
    PredicateType pt_nEnds = mapPT.get("KEY_NENDS");

    PredicateType.addKeyFunction(mapPT,"KEY_SUBS","SUBS hello");
    PredicateType pt_subs = mapPT.get("KEY_SUBS");

    PredicateType.addKeyFunction(mapPT,"KEY_NSUBS","NSUBS hello");
    PredicateType pt_nSubs = mapPT.get("KEY_NSUBS");

    PredicateType.addKeyFunction(mapPT, "KEY_CONTAINSS", "CONTAINS/S he");
    PredicateType pt_containsS = mapPT.get("KEY_CONTAINSS");

    PredicateType.addKeyFunction(mapPT, "KEY_NCONTAINSS", "NCONTAINS/S he");
    PredicateType pt_nContainsS = mapPT.get("KEY_NCONTAINSS");

    PredicateType.addKeyFunction(mapPT,"KEY_ISNULL","ISNULL");
    PredicateType pt_isNull = mapPT.get("KEY_ISNULL");

    PredicateType.addKeyFunction(mapPT,"KEY_NISNULL","NISNULL");
    PredicateType pt_nIsNull = mapPT.get("KEY_NISNULL");

    PredicateType.addKeyFunction(mapPT, "KEY_CONTAINS", "CONTAINS KEY1");
    PredicateType pt_contains = mapPT.get("KEY_CONTAINS");

    PredicateType.addKeyFunction(mapPT, "KEY_NCONTAINS", "NCONTAINS KEY1");
    PredicateType pt_nContains = mapPT.get("KEY_NCONTAINS");

    // CUSTOM
    Predicate<Double> predCustom = d -> ((d.doubleValue() < 20.0) && (d.doubleValue() >= 10.0)) ;
    PredicateType.addKeyFunction(mapPT,"KEY_CUSTOM", predCustom, Double.TYPE, "d -> ((d < 20.0) && (d > 10.0))");
    PredicateType pt_custom = mapPT.get("KEY_CUSTOM");

    // TIME - both custom and supported under expanded IF syntax
    Time timeSept21 = new Time("2022-09-21::14:15:00");
    Predicate<Time> predTimeSept21 = t -> timeSept21.equals(t);
    PredicateType.addKeyFunction(mapPT,"KEY_TIMESEPT21", predTimeSept21, Time.class, "t -> timeSept21.equals(t)");
    PredicateType pt_timeSept21 = mapPT.get("KEY_TIMESEPT21");

    PredicateType.addKeyFunction(mapPT,"KEY_TIME2CUR","MAXTIME2CUR 1");
    PredicateType pt_time2cur = mapPT.get("KEY_TIME2CUR");

    PredicateType.addKeyFunction(mapPT,"KEY_DTIME2CUR","MAXDTIME2CUR 1");
    PredicateType pt_dtime2cur = mapPT.get("KEY_DTIME2CUR");

    PredicateType.addKeyFunction(mapPT,"KEY_STDTIME2CUR","MAXSTDTIME2CUR 1");
    PredicateType pt_stdtime2cur = mapPT.get("KEY_STDTIME2CUR");

    PredicateType.addKeyFunction(mapPT,"KEY_ISOTIME2CUR","MAXISOTIME2CUR 1");
    PredicateType pt_isotime2cur = mapPT.get("KEY_ISOTIME2CUR");

    // TABLE
    Table expTable = new Table("{TABKEY1=TABVAL1,TABKEY2=2}");
    Predicate<Table> predTable = t -> expTable.equals(t);
    PredicateType.addKeyFunction(mapPT,"KEY_TABLE", predTable, Table.class, "t -> expTable.equals(t)");
    PredicateType pt7 = mapPT.get("KEY_TABLE");

    // Mix String with different expected Type
    Predicate<String> predCompare = s -> s.compareTo("aaaa") < 0;
    PredicateType.addKeyFunction(mapPT,"KEY_COMPARETO", predCompare, String.class, "s.compareTo(\"aaaa\") < 0");
    PredicateType pt_compareto = mapPT.get("KEY_COMPARETO");

    //*****************************************************************
    // Standard Filter Checks
    //*****************************************************************
    // Normal IF tests
    checkValue(pt_eq,Double.valueOf(100), true);
    checkValue(pt_eq,100.0, true);
    checkValue(pt_eq,"100", true);
    checkValue(pt_eq,70, false);
    checkValue(pt_eq,"hello",false);
    checkValue(pt_eq,null, false);
    checkValue(pt_ne,Double.valueOf(110), true);
    checkValue(pt_ne,"110", true);
    checkValue(pt_ne,Double.valueOf(100), false);
    checkValue(pt_lt,Double.valueOf(99), true);
    checkValue(pt_lt,"99.00", true);
    checkValue(pt_lt,100.0, false);
    checkValue(pt_le,100.0, true);
    checkValue(pt_le,new Data(100.0), true);
    checkValue(pt_le,Double.valueOf(120), false);
    checkValue(pt_gt,Double.valueOf(200), true);
    checkValue(pt_gt,"0200", true);
    checkValue(pt_gt,Double.valueOf(100), false);
    checkValue(pt_ge,100.0, true);
    checkValue(pt_ge,Integer.valueOf(100), true);
    checkValue(pt_ge,Double.valueOf(99), false);
    checkValue(pt_eqt,Double.valueOf(100), true);
    checkValue(pt_eqt,Double.valueOf(100-(1e-7)), true);
    checkValue(pt_eqt,"99.99999999", true);
    checkValue(pt_eqt,100-(1e-5), false);
    checkValue(pt_eqt,Double.valueOf(100+(1e-7)), true);
    checkValue(pt_eqt,Double.valueOf(100+(1e-5)), false);
    checkValue(pt_nEqt,99.0, true);
    checkValue(pt_nEqt,Double.valueOf(100-(1e-5)), true);
    checkValue(pt_nEqt,100.0-(1e-7), false);
    checkValue(pt_nEqt,"100.00000001", false);
    checkValue(pt_nEqt,Double.valueOf(100+(1e-5)), true);
    checkValue(pt_nEqt,"100.0001", true);
    checkValue(pt_nEqt,Double.valueOf(100+(1e-7)), false);
    checkValue(pt_eqs,"hello", true);
    checkValue(pt_eqs,"hi", false);
    checkValue(pt_eqs,2.0, false);
    checkValue(pt_eqs,null, false);
    checkValue(pt_nEqs,"hi", true);
    checkValue(pt_nEqs,"hello", false);
    checkValue(pt_starts,"hello",true);
    checkValue(pt_starts,"test",false);
    checkValue(pt_nStarts,"test",true);
    checkValue(pt_nStarts,"hello",false);
    checkValue(pt_ends,"hello",true);
    checkValue(pt_ends,"test",false);
    checkValue(pt_nEnds,"test",true);
    checkValue(pt_nEnds,"hello",false);
    checkValue(pt_subs,"he",true);
    checkValue(pt_subs,"test",false);
    checkValue(pt_nSubs,"test",true);
    checkValue(pt_nSubs,"he",false);
    checkValue(pt_containsS,"hello",true);
    checkValue(pt_containsS,"test",false);
    checkValue(pt_nContainsS,"test",true);
    checkValue(pt_nContainsS,"hello",false);

    Optional<String> optional = Optional.of("hi");
    checkValue(pt_isNull,null, true);
    checkValue(pt_isNull,optional, false);
    checkValue(pt_nIsNull,optional, true);
    checkValue(pt_nIsNull,null, false);

    Table tabKey1 = new Table("{KEY1=1,KEY2=HELLO}");
    Table tabNKey1 = new Table("{KEY3=HI}");
    checkValue(pt_contains,tabKey1,true);
    checkValue(pt_contains,tabNKey1,false);
    checkValue(pt_contains,"{hello",false);
    checkValue(pt_contains,2.0,false);
    checkValue(pt_contains,null,false);
    checkValue(pt_nContains,tabNKey1,true);
    checkValue(pt_nContains,tabKey1,false);


    // CUSTOM
    checkValue(pt_custom, 19.9, true);
    checkValue(pt_custom, Double.valueOf(5), false);
    checkValue(pt_custom, new Data(10), true);
    checkValue(pt_custom, Double.valueOf(21), false);

    // TIME - both custom and supported under expanded IF syntax
    checkValue(pt_timeSept21, timeSept21, true);
    Time different = new Time("2022-09-22::12:10:02");
    checkValue(pt_timeSept21, different, false);
    checkPTMapValue(mapPT,"KEY_TIMESEPT21",timeSept21, true);
    checkPTMapValue(mapPT,"KEY_TIMESEPT21",different, false);

    Time oldCurrent = Time.currentTime();
    checkValue(pt_time2cur,oldCurrent, true);
    checkValue(pt_time2cur,2.0, false);
    checkValue(pt_time2cur,null, false);
    checkPTMapValue(mapPT,"KEY_TIME2CUR",oldCurrent, true);
    checkPTMapValue(mapPT,"KEY_TIME2CUR",timeSept21, false);

    Double oldDoubleTime = Time.currentTime().getSec();
    checkValue(pt_dtime2cur,oldDoubleTime, true);
    checkPTMapValue(mapPT,"KEY_DTIME2CUR",oldDoubleTime, true);
    checkPTMapValue(mapPT,"KEY_DTIME2CUR",timeSept21.getSec(), false);

    String oldStdTime = Time.currentTime().toString(Time.STD,-1);
    checkValue(pt_stdtime2cur,oldStdTime,true);
    checkPTMapValue(mapPT,"KEY_STDTIME2CUR",oldStdTime, true);
    checkPTMapValue(mapPT,"KEY_STDTIME2CUR",timeSept21.toString(Time.STD,-1), false);

    String oldIsoTime = Time.currentTime().toString(Time.ISO8601,-1);
    checkValue(pt_isotime2cur,oldIsoTime,true);
    checkPTMapValue(mapPT,"KEY_ISOTIME2CUR",oldIsoTime, true);
    checkPTMapValue(mapPT,"KEY_ISOTIME2CUR",timeSept21.toString(Time.ISO8601,-1), false);

    // TABLE
    checkPTMapValue(mapPT,"KEY_TABLE","{TABKEY1=TABVAL1,TABKEY2=2}", true);

    // Mix String with different expected Type
    checkValue(pt_compareto, "AAAA", true);

    //*****************************************************************
    // Tests with Crazy or Potentially Problem values
    //*****************************************************************
    checkPTMapValue(mapPT,"KEY_CUSTOM",10, true);
    checkValue(pt_custom, 10, true);
    //getMidas().info("Expect one WARNing below for PredicateType Filter Check throwing an exception");
    checkValue(pt_custom, null, false);
    checkValue(pt_custom, timeSept21, false); // becomes double representation of time
    checkValue(pt_eqs,10, false);
    checkValue(pt_eqs,timeSept21, false);  // becomes STD String representation of time
    checkValue(pt7,timeSept21, false);  // becomes null Table
    checkValue(pt_timeSept21,10,false);      // Time class from double
    checkValue(pt_timeSept21,2.2949217E9,true);
  }

  private <T> void checkValue (PredicateType pt, Object testVal, boolean expected) {
    boolean passes = PredicateType.passesFilter(pt, testVal);
    boolean matchesExpected = passes == expected;
    if (!matchesExpected || printPasses) {
      String warning = (matchesExpected) ? "     " : "++++++++++++++++++++++++\nWARN FAILURE: ";
      System.out.println(warning+"testVal:"+testVal+" passes:"+ passes+" expected:"+expected+" PredicateType:"+pt);
    }
    if (useAsserts) {
      assertTrue("Predicate: "+pt.getLambdaString()+" testVal: "+testVal+" PASS/FAIL result matches expected PASS/FAIL result",matchesExpected);
    }
  }

  private void checkPTMapValue (Map<String,PredicateType> map, String key, Object testVal, boolean expected) {
    boolean passes = PredicateType.passesFilter(map, key, testVal);
    boolean matchesExpected = passes == expected;
    if (!matchesExpected || printPasses) {
      String warning = (matchesExpected) ? "     " : "========================\nWARN FAILURE: ";
      System.out.println(warning+"testVal:"+testVal+" passes:"+passes+" expected:"+expected+" map:"+ map);
    }
    if (useAsserts) {
      PredicateType pt = map.get(key);
      assertTrue("Predicate: "+pt.getLambdaString()+" testVal: "+testVal+" PASS/FAIL result matches expected PASS/FAIL result",matchesExpected);
    }
  }
}
