//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed 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 de.ugoe.cs.autoquest.genericeventmonitor;

import static org.junit.Assert.*;

import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import de.ugoe.cs.autoquest.genericeventmonitor.ClientInfos;
import de.ugoe.cs.autoquest.genericeventmonitor.GenericEvent;
import de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorMessageListener;
import de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorServer;
import de.ugoe.cs.autoquest.genericeventmonitor.GenericEventTarget;
import de.ugoe.cs.util.console.TextConsole;

/**
 *
 * @author Patrick Harms
 */
public class GenericEventMonitorServerTest implements GenericEventMonitorMessageListener {
    
    /**
     * 
     */
    public static final TextConsole CONSOLE = new TextConsole();
    
    /**
     * 
     */
    private static final int PORT = 19098;

    /**
     * 
     */
    private GenericEvent[] events;

    /**
     * 
     */
    private GenericEventTarget[] eventTargetStructures;

    /**
     * 
     */
    private String eventHandlingError;

    /**
     * 
     */
    private GenericEventMonitorServer server;

    /**
     * 
     */
    @Before
    public void setUp() throws Exception {
        server = new GenericEventMonitorServer(PORT, this);
        server.init();
        server.start();
    }
    
    /**
     * 
     */
    @After
    public void tearDown() throws Exception {
        events = null;
        eventHandlingError = null;

        if (server != null) {
            try {
                server.stop();
            }
            finally {
                server = null;
            }
        }
    }
    

    /**
     * 
     */
    @Test
    public void testSimpleMessage() throws Exception {
        String clientId = "123";
        String appId = "456";
        
        String message =
            "{" +
            "  \"message\": {" +
            "    \"clientInfos\": {" +
            "      \"clientId\":\"" + clientId + "\"," +
            "      \"appId\":\"" + appId + "\"," +
            "    }," +
            "    \"targetStructure\": [{" +
            "      \"targetId\":\"target1\"," +
            "      \"param1\":\"value1\"," +
            "      \"param3\":\"value3\"," +
            "      \"param2\":\"value2\"," +
            "      \"children\":" +
            "      [ {" +
            "          \"targetId\":\"target3\"," +
            "          \"index\":\"0\"," +
            "        }," +
            "        {" +
            "          \"targetId\":\"target2\"," +
            "          \"htmlId\":\"gsr\"," +
            "        }" +
            "      ]" +
            "    }]," +
            "    \"events\":" +
            "    [ {" +
            "        \"time\":\"12345\"," +
            "        \"targetId\":\"target2\"," +
            "        \"type\":\"gaze\"," +
            "        \"xcoordinate\": \"194\"," +
            "        \"ycoordinate\": \"12\"" +
            "      }" +
            "    ]" +
            "  }" +
            "}";
        
        sendMessageAndAssertResponse(message);
        
        if (eventHandlingError != null) {
            fail(eventHandlingError);
        }
        
        // check the target structure
        GenericEventTarget parent = null;
        
        assertEquals(1, eventTargetStructures.length);
        GenericEventTarget target = eventTargetStructures[0];
        
        assertTarget(target, parent, 2, new String[] {"param1", "value1"},
                     new String[] {"param2", "value2"},
                     new String[] {"param3", "value3"});
        
        parent = target;
        target = parent.getChildren().get(0);
        assertTarget(target, parent, 0, new String[] {"index", "0"});
        target = parent.getChildren().get(1);
        assertTarget(target, parent, 0, new String[] {"htmlId", "gsr"});

        // check the event
        assertNotNull(events);
        assertEquals(1, events.length);
        
        assertEvent(0, 12345, target, "gaze", new String[] {"xcoordinate", "194"},
                    new String[] {"ycoordinate", "12"});
        
    }

    /**
     * 
     */
    @Test
    public void testComplexMessage() throws Exception {
        String clientId = "123";
        String appId = "456";
        
        String message =
            "{" +
            "  \"message\": {" +
            "    \"clientInfos\": {" +
            "      \"clientId\":\"" + clientId + "\"," +
            "      \"appId\":\"" + appId + "\"," +
            "    }," +
            "    \"targetStructure\": [{" +
            "      \"targetId\":\"html\"," +
            "      \"index\":\"0\"," +
            "      \"children\":" +
            "      [ {" +
            "          \"targetId\":\"head\"," +
            "          \"index\":\"0\"," +
            "        }," +
            "        {" +
            "          \"tagName\":\"body\"," +
            "          \"targetId\":\"gsr\"," +
            "          \"children\":" +
            "          [ {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input1\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input2\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input3\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input4\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input5\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input6\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input7\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input8\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input9\"," +
            "            }," +
            "            {" +
            "              \"tagName\":\"input_button\"," +
            "              \"targetId\":\"input10\"," +
            "            }," +
            "          ]" +
            "        }" +
            "      ]" +
            "    }]," +
            "    \"events\":" +
            "    [ {" +
            "        \"time\":\"1\"," +
            "        \"targetId\":\"input1\"," +
            "        \"type\":\"onclick\"," +
            "        \"coordinates\": \"194\"" +
            "      }," +
            "      {" +
            "        \"time\":\"2\"," +
            "        \"targetId\":\"input2\"," +
            "        \"type\":\"ondblclick\"," +
            "        \"coordinates\": \"194\"" +
            "      }," +
            "      {" +
            "        \"time\":\"3\"," +
            "        \"targetId\":\"input3\"," +
            "        \"type\":\"onfocus\"" +
            "      }," +
            "      {" +
            "        \"time\":\"4\"," +
            "        \"targetId\":\"input4\"," +
            "        \"type\":\"onclick\"," +
            "        \"coordinates\": \"255\"" +
            "      }," +
            "      {" +
            "        \"time\":\"5\"," +
            "        \"targetId\":\"input5\"," +
            "        \"type\":\"onfocus\"" +
            "      }," +
            "      {" +
            "        \"time\":\"6\"," +
            "        \"targetId\":\"input6\"," +
            "        \"type\":\"onfocus\"" +
            "      }," +
            "      {" +
            "        \"time\":\"7\"," +
            "        \"targetId\":\"input7\"," +
            "        \"type\":\"onfocus\"" +
            "      }," +
            "      {" +
            "        \"time\":\"8\"," +
            "        \"targetId\":\"input8\"," +
            "        \"type\":\"onclick\"," +
            "        \"coordinates\": \"255\"" +
            "      }," +
            "      {" +
            "        \"time\":\"9\"," +
            "        \"targetId\":\"input9\"," +
            "        \"type\":\"onscroll\"," +
            "        \"scrollPosition\": \"194\"" +
            "      }," +
            "      {" +
            "        \"time\":\"10\"," +
            "        \"targetId\":\"input10\"," +
            "        \"type\":\"onclick\"," +
            "        \"coordinates\": \"194\"" +
            "      }" +
            "    ]" +
            "  }" +
            "}";
 
        sendMessageAndAssertResponse(message);
        
        if (eventHandlingError != null) {
            fail(eventHandlingError);
        }
        
        // check the target structure
        GenericEventTarget parent = null;
        
        assertEquals(1, eventTargetStructures.length);
        GenericEventTarget target = eventTargetStructures[0];
        
        assertTarget(target, parent, 2, new String[] {"index", "0"});

        parent = target;
        target = parent.getChildren().get(0);
        assertTarget(target, parent, 0, new String[] {"index", "0"});
        target = parent.getChildren().get(1);
        assertTarget(target, parent, 10, new String[] {"tagName", "body"});
        
        parent = target;
        for (GenericEventTarget child : parent.getChildren()) {
            assertTarget(child, parent, 0, new String[] {"tagName", "input_button"});
        }

        // check the events
        assertNotNull(events);
        assertEquals(10, events.length);

        for (int i = 0; i < events.length; i++) {
            String eventType;
            if ((i == 1)) {
                eventType = "ondblclick";
            }
            else if ((i == 2) || ((4 <= i) && (i <= 6))) {
                eventType = "onfocus";
            }
            else if ((i == 8)) {
                eventType = "onscroll";
            }
            else {
                eventType = "onclick";
            }
            
            String[] parameter = null;
            if (i <= 1) {
                parameter = new String[] { "coordinates", "194" };
            }
            else if (i == 3) {
                parameter = new String[] { "coordinates", "255" };
            }
            else if (i == 7) {
                parameter = new String[] { "coordinates", "255" };
            }
            else if (i == 8) {
                parameter = new String[] { "scrollPosition", "194" };
            }
            else if (i == 9) {
                parameter = new String[] { "coordinates", "194" };
            }
           
            if (parameter != null) {
                 assertEvent(i, i + 1, target.getChildren().get(i), eventType, parameter);
            }
            else {
                assertEvent(i, i + 1, target.getChildren().get(i), eventType);
            }
            
        }
    }
    
    /**
     * 
     */
    @Test
    public void testInvalidMessages() throws Exception {
        String message = "}";
        
        sendMessageAndAssertResponse(message);
            
        if (eventHandlingError != null) {
            fail(eventHandlingError);
        }

        assertNull(events);

        message = "blaublbidalslkdjflqkerowercalksdjfdlkkjdjfk";
        
        sendMessageAndAssertResponse(message);
            
        if (eventHandlingError != null) {
            fail(eventHandlingError);
        }

        assertNull(events);
        
        // the following message doesn't work because of the missing scroll position in the event
        message =
            "{" +
            "  \"message\": {" +
            "    \"clientInfos\": {" +
            "      \"clientId\":\"123\"," +
            "      \"userAgent\":\"Agent\"," +
            "      \"title\":\"Title\"," +
            "      \"url\":\"http://host/path\"" +
            "    }," +
            "    \"guiModel\": {" +
            "      \"tagName\":\"html\"," +
            "      \"index\":\"0\"," +
            "      \"children\":" +
            "      [ {" +
            "          \"tagName\":\"head\"," +
            "          \"index\":\"0\"," +
            "        }," +
            "        {" +
            "          \"tagName\":\"body\"," +
            "          \"htmlId\":\"gsr\"," +
            "        }" +
            "      ]" +
            "    }," +
            "    \"events\":" +
            "    [ {" +
            "        \"time\":\"9\"," +
            "        \"path\":\"/html[0]/body(htmlId=gsr)\"," +
            "        \"type\":\"onscroll\"," +
            "      }" +
            "    ]" +
            "  }" +
            "}";
        
        sendMessageAndAssertResponse(message);
        
        if (eventHandlingError != null) {
            fail(eventHandlingError);
        }

        assertNull(events);
        
        // the following message doesn't work because of the missing client id
        message =
            "{" +
            "  \"message\": {" +
            "    \"clientInfos\": {" +
            "      \"userAgent\":\"Agent\"," +
            "      \"title\":\"Title\"," +
            "      \"url\":\"http://host/path\"" +
            "    }," +
            "    \"guiModel\": {" +
            "      \"tagName\":\"html\"," +
            "      \"index\":\"0\"," +
            "      \"children\":" +
            "      [ {" +
            "          \"tagName\":\"head\"," +
            "          \"index\":\"0\"," +
            "        }," +
            "        {" +
            "          \"tagName\":\"body\"," +
            "          \"htmlId\":\"gsr\"," +
            "        }" +
            "      ]" +
            "    }," +
            "    \"events\":" +
            "    [ {" +
            "        \"time\":\"12345\"," +
            "        \"path\":\"/html[0]/body(htmlId=gsr)\"," +
            "        \"type\":\"onunload\"" +
            "      }" +
            "    ]" +
            "  }" +
            "}";
        
        sendMessageAndAssertResponse(message);
        
        if (eventHandlingError != null) {
            fail(eventHandlingError);
        }

        assertNull(events);
        
        // the following message doesn't work because of the invalid time stamp
        message =
            "{" +
            "  \"message\": {" +
            "    \"clientInfos\": {" +
            "      \"clientId\":\"123\"," +
            "      \"userAgent\":\"Agent\"," +
            "      \"title\":\"Title\"," +
            "      \"url\":\"http://host/path\"" +
            "    }," +
            "    \"guiModel\": {" +
            "      \"tagName\":\"html\"," +
            "      \"index\":\"0\"," +
            "      \"children\":" +
            "      [ {" +
            "          \"tagName\":\"head\"," +
            "          \"index\":\"0\"," +
            "        }," +
            "        {" +
            "          \"tagName\":\"body\"," +
            "          \"htmlId\":\"gsr\"," +
            "        }" +
            "      ]" +
            "    }," +
            "    \"events\":" +
            "    [ {" +
            "        \"time\":\"blub\"," +
            "        \"path\":\"/html[0]/body(htmlId=gsr)\"," +
            "        \"type\":\"onunload\"" +
            "      }" +
            "    ]" +
            "  }" +
            "}";
        
        sendMessageAndAssertResponse(message);
        
        if (eventHandlingError != null) {
            fail(eventHandlingError);
        }

        assertNull(events);
        
    }
    
    /* (non-Javadoc)
     * @see GenericEventMonitorMessageListener#handleEvents(ClientInfos, GenericEvent[])
     */
    @Override
    public void handleEvents(ClientInfos clientInfos, GenericEvent[] events) {
        if (clientInfos == null) {
            eventHandlingError = "client infos were null";
        }
        else if (events == null) {
            eventHandlingError = "events were null";
        }
        else {
            Map<String, GenericEventTarget> roots = new HashMap<>();
            
            for (GenericEvent event : events) {
                if (!clientInfos.equals(event.getClientInfos())) {
                    eventHandlingError = "one of the events did not have a correct client info";
                }
                
                GenericEventTarget parent = event.getTarget();
                
                while (parent.getParent() != null) {
                    parent = parent.getParent();
                }
                
                roots.put(parent.getId(), parent);
            }
            
            this.events = events;
            this.eventTargetStructures =
                roots.values().toArray(new GenericEventTarget[roots.size()]);
        }
    }

    /**
     *
     */
    private void sendMessageAndAssertResponse(String message) throws Exception {
        DefaultHttpClient httpclient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost("http://localhost:" + PORT + "/");
        //HttpPost httpPost = new HttpPost("http://localhost:8090/");
        HttpEntity entity = new StringEntity(message, ContentType.APPLICATION_JSON);
        httpPost.setEntity(entity);
        
        try {
            HttpResponse response = httpclient.execute(httpPost);
            
            // the monitor always returns 200 without any additional information. The client must
            // never get more or less information. This is especially important for preventing
            // hackers from finding out more
            assertEquals(200, response.getStatusLine().getStatusCode());
            assertTrue
                ((response.getEntity() == null) || (response.getEntity().getContentLength() == 0) ||
                 ((response.getEntity().getContentLength() == 1) &&
                  (response.getEntity().getContent().read() == ' ')));
        }
        finally {
            httpPost.releaseConnection();
        }
    }

    /**
     *
     */
    private void assertTarget(GenericEventTarget target,
                              GenericEventTarget parent,
                              int                noOfChildren,
                              String[] ...       parameters)
    {
        if (parent != null) {
            assertNotNull(target.getParent());
            assertEquals(parent.getId(), target.getParent().getId());
        }
        else {
            assertNull(target.getParent());
        }
        
        assertNotNull(target.getId());
        
        if (noOfChildren > 0) {
            assertNotNull(target.getChildren());
            assertEquals(noOfChildren, target.getChildren().size());
        }
        else {
            assertNull(target.getChildren());
        }
        
        assertEquals(parameters.length, target.getParameters().size());
        
        for (String[] parameter : parameters) {
            assertEquals(parameter[1], target.getParameters().get(parameter[0]));
        }
    }

    /**
     *
     */
    private void assertEvent(int                index,
                             long               timestamp,
                             GenericEventTarget target,
                             String             eventType,
                             String[] ...       parameters)
    {
        assertEquals("event " + index, Long.valueOf(timestamp), events[index].getTime());
        assertEquals("event " + index, target, events[index].getTarget());
        assertEquals("event " + index, eventType, events[index].getEventType());
        
        
        assertEquals("event " + index, parameters.length, events[index].getParameters().size());
        
        for (String[] parameter : parameters) {
            assertEquals
                ("event " + index, parameter[1], events[index].getParameters().get(parameter[0]));
        }
    }

}
