/**
 * 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 com.datatorrent.stram.webapp;

import java.util.List;
import java.util.Map;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.junit.Assert;
import org.junit.Test;

import com.google.common.collect.Maps;

import com.datatorrent.api.DefaultInputPort;
import com.datatorrent.api.DefaultOutputPort;
import com.datatorrent.api.Operator;
import com.datatorrent.common.util.BaseOperator;

public class TypeDiscoveryTest
{

  private interface GenericInterface<T>
  {
  }

  private static class StringOutputPort extends DefaultOutputPort<String>
  {
    public StringOutputPort(Operator operator)
    {
      super();
    }
  }

  private static class A<K>
  {

  }

  private static class B<T extends Number> extends A
  {

  }

  private static class GenericOutputPort extends DefaultOutputPort<B<Number>>
  {
    public GenericOutputPort(Operator operator)
    {
      super();
    }
  }

  private static class GenericSubClassOutputPort extends GenericOutputPort
  {
    public GenericSubClassOutputPort(Operator operator)
    {
      super(operator);
    }
  }

  static class ParameterizedOperator<T0, T1 extends Map<String, ? extends T0>, T2 extends Number>
      extends BaseOperator implements GenericInterface<T1>
  {
    final InputPort<T1> inputT1 = new DefaultInputPort<T1>()
    {
      @Override
      public void process(T1 tuple)
      {
      }
    };
    final OutputPort<T2> outportT2 = new DefaultOutputPort<>();
    final OutputPort<Number> outportNumberParam = new DefaultOutputPort<>();
    final StringOutputPort outportString = new StringOutputPort(this);
    final OutputPort<List<T0>> outportList = new DefaultOutputPort<>();
    final GenericSubClassOutputPort outClassObject = new GenericSubClassOutputPort(this);
  }

  static class ExtendsParameterizedOperator extends ParameterizedOperator<Number, Map<String, Double>, Double>
  {
  }

  static class SpecializedOperator extends BaseOperator
  {
    final InputPort<String> inputT1 = new DefaultInputPort<String>()
    {
      @Override
      public void process(String tuple)
      {
      }
    };
    final OutputPort<Map<String, Number>> outportT2 = new DefaultOutputPort<>();
    final OutputPort<Number> outportNumberParam = new DefaultOutputPort<>();
    final StringOutputPort outportString = new StringOutputPort(this);
  }

  public static class DiscoveryContext
  {
    JSONObject json = new JSONObject();
    Map<String, String> resolvedTypeParams = Maps.newHashMap();
  }

  @Test
  public void testTypeDiscovery() throws Exception
  {
    String[] classFilePath = OperatorDiscoveryTest.getClassFileInClasspath();
    OperatorDiscoverer od = new OperatorDiscoverer(classFilePath);
    od.buildTypeGraph();

    JSONObject Desc = od.describeClassByASM(ExtendsParameterizedOperator.class.getName());
    JSONArray json = Desc.getJSONArray("portTypeInfo");

    String debug = "\n(ASM)type info for " + ExtendsParameterizedOperator.class + ":\n" + Desc.toString(2) + "\n";

    ObjectMapper mapper = new ObjectMapper();
    //List<?> l = mapper.convertValue(json, List.class);
    JsonNode root = mapper.readTree(json.toString(2));
    String val = root.get(0).path("name").asText();
    Assert.assertEquals(debug + "port name", "inputT1", val);

    val = root.get(3).path("name").asText();
    Assert.assertEquals(debug + "port name", "outportString", val);

    val = root.get(3).path("type").asText();
    Assert.assertEquals(debug + "outportList type", "java.lang.String", val);

    val = root.get(4).path("name").asText();
    Assert.assertEquals(debug + "port name", "outportList", val);

    val = root.get(4).path("type").asText();
    Assert.assertEquals(debug + "outportList type", "java.util.List", val);

    val = root.get(4).path("typeArgs").get(0).path("type").asText();
    Assert.assertEquals(debug + "outportList type", "java.lang.Number", val);

    val = root.get(5).path("name").asText();
    Assert.assertEquals(debug + "port name", "outClassObject", val);

    val = root.get(5).path("type").asText();
    Assert.assertEquals(debug + "outportList type", "com.datatorrent.stram.webapp.TypeDiscoveryTest$B", val);

    val = root.get(5).path("typeArgs").get(0).path("type").asText();
    Assert.assertEquals(debug + "outportList type", "java.lang.Number", val);
  }

  static class ParameterizedTypeOperator<T> extends BaseOperator
  {
    final OutputPort<T> output = new DefaultOutputPort<>();
  }

  static class StringParameterOperator extends ParameterizedTypeOperator<String>
  {
  }

  @Test
  public void testTypeDiscovery2() throws Exception
  {
    String[] classFilePath = OperatorDiscoveryTest.getClassFileInClasspath();
    OperatorDiscoverer od = new OperatorDiscoverer(classFilePath);
    od.buildTypeGraph();

    JSONObject Desc = od.describeClassByASM(StringParameterOperator.class.getName());
    JSONArray json = Desc.getJSONArray("portTypeInfo");
    String debug = "\n(ASM)type info for " + StringParameterOperator.class + ":\n" + Desc.toString(2) + "\n";

    ObjectMapper mapper = new ObjectMapper();
    JsonNode root = mapper.readTree(json.toString(2));
    String val = root.get(0).path("name").asText();
    Assert.assertEquals(debug + "port name", "output", val);

    val = root.get(0).path("type").asText();
    Assert.assertEquals(debug + "port type", "java.lang.String", val);
  }

  static class SubClass<K> extends ParameterizedTypeOperator<K>
  {
  }

  static class SubSubClass extends SubClass<Map<String, Object>>
  {
    public String trial;
  }

  @Test
  public void testTypeDiscoveryMultiLevel() throws Exception
  {
    String[] classFilePath = OperatorDiscoveryTest.getClassFileInClasspath();
    OperatorDiscoverer od = new OperatorDiscoverer(classFilePath);
    od.buildTypeGraph();

    JSONObject Desc = od.describeClassByASM(SubSubClass.class.getName());
    JSONArray json = Desc.getJSONArray("portTypeInfo");
    String debug = "\n(ASM)type info for " + SubSubClass.class + ":\n" + Desc.toString(2) + "\n";

    ObjectMapper mapper = new ObjectMapper();
    JsonNode root = mapper.readTree(json.toString(2));
    String val = root.get(0).path("name").asText();
    Assert.assertEquals(debug + "port name", "output", val);

    val = root.get(0).path("type").asText();
    Assert.assertEquals(debug + "port type", "java.util.Map", val);

    val = root.get(0).path("typeArgs").get(0).path("type").asText();
    Assert.assertEquals(debug + "map key type", "java.lang.String", val);
    val = root.get(0).path("typeArgs").get(1).path("type").asText();
    Assert.assertEquals(debug + "map value type", "java.lang.Object", val);
  }

  @Test
  public void testAppAttributes() throws JSONException, IllegalAccessException
  {
    JSONArray appAttributes = TypeDiscoverer.getAppAttributes().getJSONArray("attributes");
    Map<String, JSONObject> attributesMap = Maps.newHashMap();
    for (int i = 0; i < appAttributes.length(); i++) {
      attributesMap.put(appAttributes.getJSONObject(i).getString("name"), appAttributes.getJSONObject(i));
    }
    JSONObject appNameAttr = attributesMap.get("APPLICATION_NAME");
    Assert.assertNotNull("application name", appNameAttr);
    Assert.assertEquals("application name type", "java.lang.String", appNameAttr.getString("type"));
    Assert.assertNotNull("default app name", appNameAttr.getString("default"));

    JSONObject stringCodecsAttr = attributesMap.get("STRING_CODECS");
    Assert.assertNotNull("string codecs", stringCodecsAttr);
    Assert.assertEquals("string codecs type", "java.util.Map", stringCodecsAttr.getString("type"));
    Assert.assertNotNull("type args", stringCodecsAttr.getJSONArray("typeArgs"));
  }

  @Test
  public void testOperatorAttributes() throws JSONException, IllegalAccessException
  {
    JSONArray operatorAttributes = TypeDiscoverer.getOperatorAttributes().getJSONArray("attributes");
    Map<String, JSONObject> attributesMap = Maps.newHashMap();
    for (int i = 0; i < operatorAttributes.length(); i++) {
      attributesMap.put(operatorAttributes.getJSONObject(i).getString("name"), operatorAttributes.getJSONObject(i));
    }
    JSONObject activationWindowAttr = attributesMap.get("ACTIVATION_WINDOW_ID");
    Assert.assertNotNull("activation window", activationWindowAttr);
    Assert.assertEquals("activation window type", "java.lang.Long", activationWindowAttr.getString("type"));
    Assert.assertEquals("default activation window", "-1", activationWindowAttr.getString("default"));

    JSONObject partitionerAttr = attributesMap.get("PARTITIONER");
    Assert.assertNotNull("partitioner", partitionerAttr);
    Assert.assertEquals("partitioner type", "com.datatorrent.api.Partitioner", partitionerAttr.getString("type"));
    Assert.assertNotNull("type args", partitionerAttr.getJSONArray("typeArgs"));

    JSONObject processingModeAttr = attributesMap.get("PROCESSING_MODE");
    Assert.assertNotNull("processingModeAttr", processingModeAttr);
    Assert.assertEquals("processing mode type", "com.datatorrent.api.Operator$ProcessingMode", processingModeAttr.getString("type"));
    Assert.assertEquals("ui type", "Enum", processingModeAttr.getString("uiType"));
  }

  @Test
  public void testPortAttributes() throws JSONException, IllegalAccessException
  {
    JSONArray portAttributes = TypeDiscoverer.getPortAttributes().getJSONArray("attributes");
    Map<String, JSONObject> attributesMap = Maps.newHashMap();
    for (int i = 0; i < portAttributes.length(); i++) {
      attributesMap.put(portAttributes.getJSONObject(i).getString("name"), portAttributes.getJSONObject(i));
    }
    JSONObject queueCapacityAttr = attributesMap.get("QUEUE_CAPACITY");
    Assert.assertNotNull("queue capacity", queueCapacityAttr);
    Assert.assertEquals("queue capacity type", "java.lang.Integer", queueCapacityAttr.getString("type"));
    Assert.assertEquals("default queue capacity", "1024", queueCapacityAttr.getString("default"));

    JSONObject streamCodecAttr = attributesMap.get("STREAM_CODEC");
    Assert.assertNotNull("stream codec", streamCodecAttr);
    Assert.assertEquals("stream codec type", "com.datatorrent.api.StreamCodec", streamCodecAttr.getString("type"));
    Assert.assertNotNull("type args", streamCodecAttr.getJSONArray("typeArgs"));
  }

}