/*
 * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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 io.siddhi.core.window;

import io.siddhi.core.SiddhiAppRuntime;
import io.siddhi.core.SiddhiManager;
import io.siddhi.core.event.Event;
import io.siddhi.core.exception.CannotRestoreSiddhiAppStateException;
import io.siddhi.core.exception.SiddhiAppCreationException;
import io.siddhi.core.query.output.callback.QueryCallback;
import io.siddhi.core.stream.input.InputHandler;
import io.siddhi.core.stream.output.StreamCallback;
import io.siddhi.core.util.EventPrinter;
import io.siddhi.core.util.SiddhiTestHelper;
import io.siddhi.core.util.persistence.InMemoryPersistenceStore;
import io.siddhi.core.util.persistence.PersistenceStore;
import org.apache.log4j.Logger;
import org.testng.Assert;
import org.testng.AssertJUnit;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Delay window implementation testcase
 */

public class DelayWindowTestCase {
    private static final Logger log = Logger.getLogger(DelayWindowTestCase.class);
    private boolean eventArrived;
    private AtomicInteger count = new AtomicInteger(0);
    private Long lastValue;
    private double averageValue;
    private int removeEventCount;
    private int inEventCount;
    private boolean error;

    @BeforeMethod
    public void init() {
        count.set(0);
        eventArrived = false;
        lastValue = Long.valueOf(0);
        averageValue = Double.valueOf(0);
        removeEventCount = 0;
        inEventCount = 0;
        error = true;
    }

    @Test(description = "Check if Siddhi App is created successfully when only one parameter of type either " +
            "int or long is specified")
    public void delayWindowTest0() {
        log.info("DelayWindow Test0 : Testing window parameter definition1");
        SiddhiManager siddhiManager = new SiddhiManager();
        String query = "define window eventWindow(symbol string, price int, volume float) delay(1 hour)";
        SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(query);
        siddhiAppRuntime.shutdown();
    }

    @Test(description = "Check if Siddhi App Creation fails when more than one parameter is included",
            expectedExceptions = SiddhiAppCreationException.class)
    public void delayWindowTest1() {
        log.info("DelayWindow Test1 : Testing window parameter definition2");
        SiddhiManager siddhiManager = new SiddhiManager();
        String query = "define window eventWindow(symbol string, price int, volume float) delay(2,3) ";
        SiddhiAppRuntime siddhiAppRuntime = null;
        try {
            siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(query);
        } catch (SiddhiAppCreationException e) {
            error = false;
            AssertJUnit.assertEquals("There is no parameterOverload for 'delay' that matches attribute types " +
                            "'<INT, INT>'. Supported parameter overloads are (<INT|LONG|TIME> window.delay).",
                    e.getCause().getMessage());
            throw e;
        } finally {
            if (siddhiAppRuntime != null) {
                siddhiAppRuntime.shutdown();
            }
        }
    }

    @Test(description = "Check if Siddhi App Creation fails when the type of parameter is neither int or long",
            expectedExceptions = SiddhiAppCreationException.class)
    public void delayWindowTest2() {
        log.info("DelayWindow Test2 : Testing window parameter definition3");
        SiddhiManager siddhiManager = new SiddhiManager();
        String query = "define window eventWindow(symbol string, price int, volume float) delay('abc') ";
        SiddhiAppRuntime siddhiAppRuntime = null;
        try {
            siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(query);
        } catch (SiddhiAppCreationException e) {
            error = false;
            AssertJUnit.assertEquals("There is no parameterOverload for 'delay' that matches attribute types " +
                            "'<STRING>'. Supported parameter overloads are (<INT|LONG|TIME> window.delay).",
                    e.getCause().getMessage());
            throw e;
        } finally {
            if (siddhiAppRuntime != null) {
                siddhiAppRuntime.shutdown();
            }
        }
    }

    @Test(description = "Check whether the events are processed when using delay window")
    public void delayWindowTest3() throws InterruptedException {
        log.info("DelayWindow Test3: Testing delay window event processing");

        SiddhiManager siddhiManager = new SiddhiManager();

        String cseEventStream = "" +
                "define stream cseEventStream (symbol string, price float, volume int); " +
                "define window csEventWindow (symbol string, price float, volume int) delay(2 sec) output all events;";

        String query = "" +
                "@info(name = 'query0') " +
                "from cseEventStream " +
                "insert into csEventWindow; " +
                "" +
                "@info(name = 'query1') " +
                "from csEventWindow " +
                "select symbol,price,volume " +
                "insert all events into outputStream;";

        SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(cseEventStream + query);

        siddhiAppRuntime.addCallback("outputStream", new StreamCallback() {
            @Override
            public void receive(Event[] events) {
                EventPrinter.print(events);
                count.addAndGet(events.length);
                eventArrived = true;
                for (Event event : events) {
                    AssertJUnit.assertTrue(("IBM".equals(event.getData(0)) || "WSO2".equals(event.getData(0))));
                }
            }
        });

        InputHandler input = siddhiAppRuntime.getInputHandler("cseEventStream");
        siddhiAppRuntime.start();

        input.send(new Object[]{"IBM", 700f, 0});
        input.send(new Object[]{"IBM", 750f, 0});
        input.send(new Object[]{"IBM", 800f, 0});
        input.send(new Object[]{"WSO2", 60.5f, 1});

        SiddhiTestHelper.waitForEvents(100, 4, count, 4000);
        AssertJUnit.assertEquals(4, count.get());
        AssertJUnit.assertTrue(eventArrived);
        siddhiAppRuntime.shutdown();
    }

    @Test(description = "Check whether delay window join is working properly")
    public void delayWindowTest4() throws InterruptedException {
        log.info("DelayWindow Test4 : Testing delay window joins");

        SiddhiManager siddhiManager = new SiddhiManager();
        String streams = "" +
                "define stream cseEventStream (symbol string, price float, volume int); " +
                "define stream twitterStream (user string, tweet string, company string); ";
        String query = "" +
                "@info(name = 'query1') " +
                "from cseEventStream#window.delay(1 sec) join twitterStream#window.delay(1 sec) " +
                "on cseEventStream.symbol== twitterStream.company " +
                "select cseEventStream.symbol as symbol, twitterStream.tweet, cseEventStream.price " +
                "insert all events into outputStream ;";

        SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(streams + query);
        try {
            siddhiAppRuntime.addCallback("outputStream", new StreamCallback() {
                @Override
                public void receive(Event[] events) {
                    EventPrinter.print(events);
                    if (events != null) {
                        count.addAndGet(events.length);
                    }
                    AssertJUnit.assertTrue("WSO2".equals(events[0].getData(0)) || "IBM".equals(events[0].getData(0)));
                    eventArrived = true;
                }
            });
            InputHandler cseEventStreamHandler = siddhiAppRuntime.getInputHandler("cseEventStream");
            InputHandler twitterStreamHandler = siddhiAppRuntime.getInputHandler("twitterStream");
            siddhiAppRuntime.start();

            cseEventStreamHandler.send(new Object[]{"WSO2", 55.6f, 100});
            cseEventStreamHandler.send(new Object[]{"IBM", 75.6f, 100});
            twitterStreamHandler.send(new Object[]{"User1", "Hello World", "WSO2"});
            twitterStreamHandler.send(new Object[]{"User2", "Hello World2", "IBM"});

            Thread.sleep(1100);
            cseEventStreamHandler.send(new Object[]{"WSO2", 57.6f, 100});

            SiddhiTestHelper.waitForEvents(100, 2, count, 5000);
            AssertJUnit.assertEquals(2, count.get());
            AssertJUnit.assertTrue(eventArrived);
        } finally {
            siddhiAppRuntime.shutdown();
        }
    }

    @Test(description = "Check whether aggregations are done correctly when using delay window ")
    public void delayWindowTest5() {
        log.info("DelayWindow Test5 : Testing delay window for Aggregation");

        SiddhiManager siddhiManager = new SiddhiManager();

        String eventStream = "" +
                "define stream CargoStream (weight int); " +
                "define stream OutputStream(weight int, totalWeight long, averageWeight double); ";
        String query = "" +
                "@info(name='CargoWeightQuery') " +
                "from CargoStream#window.delay(1 sec) " +
                "select weight, sum(weight) as totalWeight, avg(weight) as averageWeight " +
                "insert into OutputStream;";

        SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(eventStream + query);

        siddhiAppRuntime.addCallback("CargoWeightQuery", new QueryCallback() {
            @Override
            public void receive(long timestamp, Event[] inEvents, Event[] removeEvents) {
                EventPrinter.print(timestamp, inEvents, removeEvents);
                if (inEvents != null) {
                    inEventCount = inEventCount + inEvents.length;
                }
                if (removeEvents != null) {
                    removeEventCount = removeEventCount + removeEvents.length;
                }
                eventArrived = true;
            }

        });

        StreamCallback callBack = new StreamCallback() {
            @Override
            public void receive(Event[] events) {
                for (Event event : events) {
                    lastValue = (Long) event.getData(1);
                    averageValue = (Double) event.getData(2);
                }
            }
        };

        siddhiAppRuntime.addCallback("OutputStream", callBack);

        InputHandler inputHandler = siddhiAppRuntime.getInputHandler("CargoStream");
        siddhiAppRuntime.start();
        try {
            inputHandler.send(new Object[]{1000});
            Thread.sleep(100);
            inputHandler.send(new Object[]{1500});

            SiddhiTestHelper.waitForEvents(100, 2, count, 3000);
            AssertJUnit.assertEquals(2, inEventCount);
            AssertJUnit.assertEquals(0, removeEventCount);
            AssertJUnit.assertEquals(Long.valueOf(2500), lastValue);
            AssertJUnit.assertEquals(Double.valueOf(1250), averageValue);

        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            siddhiAppRuntime.shutdown();
        }
    }

    @Test(description = "Check whether the events are being actually delayed, for the given time period.")
    public void delayWindowTest6() throws InterruptedException {
        log.info("DelayWindow Test6: Testing delay time ");

        SiddhiManager siddhiManager = new SiddhiManager();

        String cseEventStream = "" +
                "define window delayWindow(symbol string, volume int) delay(1450);" +
                "define stream inputStream(symbol string, volume int);" +
                "define window timeWindow(symbol string) time(2 sec);";

        String query = "" +
                "@info(name='query1') " +
                "from inputStream " +
                "select symbol, volume " +
                "insert into delayWindow; " +
                "" +
                "@info(name = 'query2') " +
                "from delayWindow " +
                "select symbol, volume " +
                "insert into analyzeStream; " +
                "" +
                "@info(name='query3') " +
                "from inputStream " +
                "select symbol " +
                "insert into timeWindow; " +
                "" +
                "@info(name='query4') " +
                "from analyzeStream join timeWindow " +
                "on analyzeStream.symbol == timeWindow.symbol " +
                "select analyzeStream.symbol " +
                "insert into outputStream; ";

        SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(cseEventStream + query);

        siddhiAppRuntime.addCallback("outputStream", new StreamCallback() {
            @Override
            public void receive(Event[] events) {
                EventPrinter.print(events);
                count.addAndGet(events.length);
                eventArrived = true;
                for (Event event : events) {
                    AssertJUnit.assertTrue(("IBM".equals(event.getData(0)) || "WSO2".equals(event.getData(0))));
                }
            }
        });

        InputHandler input = siddhiAppRuntime.getInputHandler("inputStream");
        siddhiAppRuntime.start();

        input.send(new Object[]{"IBM", 700});
        input.send(new Object[]{"WSO2", 750});

        SiddhiTestHelper.waitForEvents(100, 2, count, 5000);
        AssertJUnit.assertEquals(2, count.get());
        AssertJUnit.assertTrue(eventArrived);
        siddhiAppRuntime.shutdown();
    }

    @Test(description = "Check if events are persisted when using delay window")
    public void delayWindowTest7() throws InterruptedException {
        log.info("DelayWindow Test7: Testing persistence ");

        PersistenceStore persistenceStore = new InMemoryPersistenceStore();

        SiddhiManager siddhiManager = new SiddhiManager();
        siddhiManager.setPersistenceStore(persistenceStore);

        String cseEventStream = "" +
                "define window delayWindow(symbol string, volume int) delay(1450);" +
                "define stream inputStream(symbol string, volume int);" +
                "define window timeWindow(symbol string) time(2 sec);";

        String query = "" +
                "@info(name='query1') " +
                "from inputStream " +
                "select symbol, volume " +
                "insert into delayWindow; " +
                "" +
                "@info(name = 'query2') " +
                "from delayWindow " +
                "select symbol, sum(volume) as totalVolume " +
                "insert into analyzeStream; " +
                "" +
                "@info(name='query3') " +
                "from inputStream " +
                "select symbol " +
                "insert into timeWindow; " +
                "" +
                "@info(name='query4') " +
                "from analyzeStream join timeWindow " +
                "on analyzeStream.symbol == timeWindow.symbol " +
                "select analyzeStream.symbol, analyzeStream.totalVolume " +
                "insert into outputStream; ";

        SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(cseEventStream + query);

        siddhiAppRuntime.addCallback("outputStream", new StreamCallback() {
            @Override
            public void receive(Event[] events) {
                EventPrinter.print(events);
                count.addAndGet(events.length);
                for (Event event : events) {
                    AssertJUnit.assertTrue(("IBM".equals(event.getData(0)) || "WSO2".equals(event.getData(0))));
                }
                lastValue = (Long) events[0].getData(1);
            }
        });

        InputHandler input = siddhiAppRuntime.getInputHandler("inputStream");
        siddhiAppRuntime.start();

        input.send(new Object[]{"IBM", 700});
        input.send(new Object[]{"WSO2", 750});

        SiddhiTestHelper.waitForEvents(100, 2, count, 4000);

        siddhiAppRuntime.persist();
        siddhiAppRuntime.shutdown();

        input = siddhiAppRuntime.getInputHandler("inputStream");
        siddhiAppRuntime.start();

        try {
            siddhiAppRuntime.restoreLastRevision();
        } catch (CannotRestoreSiddhiAppStateException e) {
            Assert.fail("Restoring of Siddhi app " + siddhiAppRuntime.getName() + " failed");
        }

        input.send(new Object[]{"WSO2", 600});
        SiddhiTestHelper.waitForEvents(100, 3, count, 4000);

        AssertJUnit.assertEquals(Long.valueOf(2050), lastValue);
        siddhiAppRuntime.shutdown();
    }

    @Test(expectedExceptions = SiddhiAppCreationException.class)
    public void delayWindowTest8() {
        log.info("DelayWindow Test8 : Testing delay window for dinamic value");
        SiddhiManager siddhiManager = new SiddhiManager();
        String eventStream = "" +
                "define stream CargoStream (weight int); " +
                "define stream OutputStream(weight int, totalWeight long, averageWeight double); ";
        String query = "" +
                "@info(name='CargoWeightQuery') " +
                "from CargoStream#window.delay(1/2) " +
                "select weight, sum(weight) as totalWeight, avg(weight) as averageWeight " +
                "insert into OutputStream;";
        SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(eventStream + query);
        siddhiAppRuntime.addCallback("CargoWeightQuery", new QueryCallback() {
            @Override
            public void receive(long timestamp, Event[] inEvents, Event[] removeEvents) {
                EventPrinter.print(timestamp, inEvents, removeEvents);
                if (inEvents != null) {
                    inEventCount = inEventCount + inEvents.length;
                }
                if (removeEvents != null) {
                    removeEventCount = removeEventCount + removeEvents.length;
                }
                eventArrived = true;
            }

        });
        StreamCallback callBack = new StreamCallback() {
            @Override
            public void receive(Event[] events) {
                for (Event event : events) {
                    lastValue = (Long) event.getData(1);
                    averageValue = (Double) event.getData(2);
                }
            }
        };
        siddhiAppRuntime.addCallback("OutputStream", callBack);
        InputHandler inputHandler = siddhiAppRuntime.getInputHandler("CargoStream");
        siddhiAppRuntime.start();
        try {
            inputHandler.send(new Object[]{1000});
            inputHandler.send(new Object[]{1500});

            SiddhiTestHelper.waitForEvents(100, 2, count, 3000);
            AssertJUnit.assertEquals(2, inEventCount);
            AssertJUnit.assertEquals(0, removeEventCount);
            AssertJUnit.assertEquals(Long.valueOf(2500), lastValue);
            AssertJUnit.assertEquals(Double.valueOf(1250), averageValue);

        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            siddhiAppRuntime.shutdown();
        }
    }
}