package nxm.net.test.junit.lib;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;

import nxm.net.lib.DatagramData;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.lib.XmlFile;
import nxm.sys.test.junit.NMTestCase;

/** Test cases for DatagramData.
    @author  J. Czechowski IV
 */
public final class DatagramDataTest extends NMTestCase {

  private static boolean runFromMain = false; // false means run as junit, not from main()
  private static HashMap<String, TreeSet<Integer>> results = new HashMap<>(); // added for asserts to be done outside of threads - since 3.9.1
  private boolean expectFail = false;
  // The following four are used for testing settable max packer buffer size
  /** maximum buffer size we successfully read */
  private static int maxBufLength = 0;
  /** default max as defined in DatagramData */
  private static final int MAXPKTBUF=32768;
  /** size that is too small for some of the data that requires 45 or greater size */
  private static final int SMALLPKTBUF=44;
  /** the orignal max, so we can restore it */
  private static int origPktBufSize=DatagramData.getPktBufferSize();

  /** Creates a new instance of the class.
      @param methodName The name of the method being called.
   */
  public DatagramDataTest(String methodName) {
    super(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 DatagramDataTest(String methodName, Midas midas) {
    super(methodName, midas);
  }

  /**
   * Test the read() and other methods
   * @since 3.7.2 have test run as junit test
   */
  public void testRead () {
    boolean isNeXtOpt = Shell.isNeXtOpt();
    if (!isNeXtOpt) {
      try {
        DatagramData.setPktBufferSize(MAXPKTBUF);
        doTest();
        // Test with setting a max packet buffer size that is
        // too small and will throw an exception
        expectFail = true;
        maxBufLength = 0;
        DatagramData.setPktBufferSize(SMALLPKTBUF);
        doTest();
      } catch (Exception e) {
        if (!expectFail) {
          fail( "Failed with exception:"+e);
        }
        else {
          assertEquals("Setting max paket buf size", SMALLPKTBUF, maxBufLength);
        }
      }
      finally {
        DatagramData.setPktBufferSize(origPktBufSize);
      }
    }
  }

  public static class MulticastWriter extends Thread {
    String         name;
    int            count;
    double         pause;
    DatagramSocket socket;
    InetAddress    address;
    int            port;
    boolean        done;
    Throwable      error;

    public MulticastWriter (String name, int count, double pause, InetAddress address, MulticastSocket socket, int port) throws Exception {
      super("MulticastWriter: "+name);
      this.name    = name;
      this.count   = count;
      this.pause   = pause;
      this.socket  = socket;
      this.address = address;
      this.port    = port;
      this.done    = false;
      this.error   = null;
      // updated in 3.9.3 for NXM-3548
      InetSocketAddress isa = new InetSocketAddress(address, port);
      socket.joinGroup(isa, socket.getNetworkInterface());
    }

    public void run () {
      try {
        for (int i = 0; i < count; i++) {
          String xml = "<XML><MESSAGE NAME=\""+name+"\" COUNT=\""+i+"\"/></XML>";
          byte[] buf = xml.getBytes();
          DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);

          socket.send(packet);
          Time.sleep(pause);
        }
      } catch (Exception e) {
        error = e;
      }
      done = true;
    }

    public void waitFor () {
      while (!done) {
        Time.sleep(0.1);
      }
      if (error != null) {
        throw new RuntimeException("Error writing to socket on thread "+this, error);
      }
    }
  }

  public static class MulticastReader extends Thread {
    String         name;
    DatagramData   dgd;
    boolean        stop;
    boolean        done;
    Throwable      error;
    TreeSet<Integer>   packetValues; // hold the packet values for later asserts - since 3.9.1

    public MulticastReader (String name, String host, int port) {
      super("MulticastReader: "+name);
      this.name   = name;
      this.dgd    = new DatagramData(host, port);
      this.done   = false;
      this.error  = null;
      this.packetValues = new TreeSet<>();

//      dgd.setTimeOut(5000);
      dgd.setTimeOut(100);
      dgd.setSAP(false);
      dgd.setRTP(false);
      if (!dgd.open()) {
        throw new MidasException("Could not open DatagramData with host="+host+" port="+port);
      }
    }

    public void run () {
      try {
        String expName  = name;
        int    expCount = 0;

        while (!stop) {
          byte[] buf = dgd.read();

          if (buf != null) {
            // leep track of max for test with setting too small max packet buffer size
            if (buf.length > maxBufLength) maxBufLength = buf.length;
            String xml      = new String(buf);
            Table  tbl      = XmlFile.fastXmlToTable(xml, "_", "CDATA", 0, true);
            String actName  = tbl.getS("MESSAGE.NAME", "ERROR");
            int    actCount = tbl.getL("MESSAGE.COUNT", -1);
            packetValues.add(actCount); // add the count from the packet to the packetvalues set since 3.9.1
            if (DatagramDataTest.runFromMain) {
              // run not as junit, but from main() for debug
              if (!actName.equals(expName)) {
                Shell.warning(name+": expected "+expName+" "+expCount+" got "+actName+" "+actCount);
              }
              else if (actCount != expCount) {
                Shell.warning(name+": expected "+expName+" "+expCount+" got "+actName+" "+actCount);
              }
              else {
                Shell.info(name+": got "+actName+" "+actCount);
                expCount++;
              }
            }
            else {
              // run as junit test

              // multicast is run as udp, this is causing issues with packets being out of order
              // To test it in a more reliable fashion - add the packets to a set and check the size of the set is the right size and the largest number is the count - 1
//              assertEquals("message name",expName,actName);
//              assertEquals("message conut",expCount,actCount);
              expCount++; // Not needed but keeping if wanted in the future
            }
          }
        }
      } catch (Exception e) {
        error = e;
      }
      done = true;
      results.put(name, packetValues); // at the end, put the packetValues set into the results map for asserts after completion since 3.9.1
    }

    public void waitFor () {
      stop = true;
      while (!done) {
        Time.sleep(0.1);
      }
      if (error != null) {
        throw new RuntimeException("Error writing to socket on thread "+this, error);
      }
    }
  }


  public static void main (String[] args) throws Exception {
    runFromMain = true;
    doTest();
  }

  public static void doTest () throws Exception {
    MulticastWriter[] writers = new MulticastWriter[4];
    MulticastReader[] readers = new MulticastReader[writers.length];

    int    count = 128;
    double pause = 0.01;

    for (int i = 0; i < writers.length; i++) {
      String name = "NUM"+i;
      String host = "232.0.1."+i;
      int    port = 9000;
      if (runFromMain) Shell.info("host = "+host+" port = "+port);

      InetAddress     address = InetAddress.getByName(host);
      MulticastSocket socket  = new MulticastSocket(port);

      writers[i] = new MulticastWriter(name, count, pause, address, socket, port);
      readers[i] = new MulticastReader(name, host, port);
    }

    if (runFromMain) Shell.info("START");
    for (int i = 0; i < readers.length; i++) {
      readers[i].start();
    }
    for (int i = 0; i < writers.length; i++) {
      writers[i].start();
    }
    for (int i = 0; i < writers.length; i++) {
      writers[i].waitFor();
    }
    for (int i = 0; i < readers.length; i++) {
      readers[i].waitFor();
    }

    if (runFromMain) Shell.info("DONE");
    else {
      // run as unittest, lets make sure the results are correct since 3.9.1
      for(Map.Entry<String, TreeSet<Integer>> entry : results.entrySet()) {
        if (entry.getValue().size() == 0) {
          // assume alternate test environment where this does not work for now
          //
          Shell.getMidasContext().warning(
              "************************************************************\n"+
              "       DatagramData.doTest() NO PACKETS RECEIVED\n"+
              "        May be test environment issue\n"+
              "      ************************************************************");
        }
        else {
          assertEquals("Messages recieved should be " + count + " for thread " + entry.getKey(), count,
              entry.getValue().size());
          assertEquals("The last highest count found in the message should be " + entry.getValue().last() + " for thread "
              + entry.getKey(), Integer.valueOf(count - 1), entry.getValue().last());
        }
      }
    }
  }

}
