/*
 * 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 org.apache.unomi.itests;

import org.apache.unomi.api.*;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.conditions.ConditionType;
import org.apache.unomi.api.rules.Rule;
import org.apache.unomi.api.services.EventService;
import org.apache.unomi.api.services.RulesService;
import org.apache.unomi.persistence.spi.CustomObjectMapper;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.junit.PaxExam;
import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
import org.ops4j.pax.exam.spi.reactors.PerSuite;
import org.ops4j.pax.exam.util.Filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.*;

import static org.junit.Assert.*;

/**
 * Integration tests for the Unomi rule service.
 */
@RunWith(PaxExam.class)
@ExamReactorStrategy(PerSuite.class)
public class RuleServiceIT extends BaseIT {

    private final static Logger LOGGER = LoggerFactory.getLogger(RuleServiceIT.class);

    private final static String TEST_RULE_ID = "test-rule-id";
    public static final String TEST_SCOPE = "test-scope";

    @Inject
    @Filter(timeout = 600000)
    protected RulesService rulesService;

    @Inject
    @Filter(timeout = 600000)
    protected EventService eventService;

    @Before
    public void setUp() {
        TestUtils.removeAllProfiles(definitionsService, persistenceService);
    }

    @Test
    public void testRuleWithNullActions() throws InterruptedException {
        Metadata metadata = new Metadata(TEST_RULE_ID);
        metadata.setName(TEST_RULE_ID + "_name");
        metadata.setDescription(TEST_RULE_ID + "_description");
        metadata.setScope(TEST_SCOPE);
        Rule nullRule = new Rule(metadata);
        nullRule.setCondition(null);
        nullRule.setActions(null);
        createAndWaitForRule(nullRule);
        assertNull("Expected rule actions to be null", nullRule.getActions());
        assertNull("Expected rule condition to be null", nullRule.getCondition());
        assertEquals("Invalid rule name", TEST_RULE_ID + "_name", nullRule.getMetadata().getName());
        rulesService.removeRule(TEST_RULE_ID);
        refreshPersistence();
        rulesService.refreshRules();
    }

    @Test
    public void testRuleEventTypeOptimization() throws InterruptedException {

        ConditionBuilder builder = new ConditionBuilder(definitionsService);
        Rule simpleEventTypeRule = new Rule(new Metadata(TEST_SCOPE, "simple-event-type-rule", "Simple event type rule", "A rule with a simple condition to match an event type"));
        simpleEventTypeRule.setCondition(builder.condition("eventTypeCondition").parameter("eventTypeId", "view").build());
        createAndWaitForRule(simpleEventTypeRule);
        Rule complexEventTypeRule = new Rule(new Metadata(TEST_SCOPE, "complex-event-type-rule", "Complex event type rule", "A rule with a complex condition to match multiple event types with negations"));
        complexEventTypeRule.setCondition(
                builder.not(
                        builder.or(
                                builder.condition("eventTypeCondition").parameter( "eventTypeId", "view"),
                                builder.condition("eventTypeCondition").parameter("eventTypeId", "form")
                        )
                ).build()
        );
        createAndWaitForRule(complexEventTypeRule);
        Rule noEventTypeRule = new Rule(new Metadata(TEST_SCOPE, "no-event-type-rule", "No event type rule", "A rule with a simple condition but no event type matching"));
        noEventTypeRule.setCondition(builder.condition("eventPropertyCondition")
                .parameter("propertyName", "target.properties.pageInfo.language")
                .parameter("comparisonOperator", "equals")
                .parameter("propertyValue", "en")
                .build());
        createAndWaitForRule(noEventTypeRule);

        Profile profile = new Profile(UUID.randomUUID().toString());
        Session session = new Session(UUID.randomUUID().toString(), profile, new Date(), TEST_SCOPE);
        Event viewEvent = generateViewEvent(session, profile);
        Set<Rule> matchingRules = rulesService.getMatchingRules(viewEvent);

        assertTrue("Simple rule should be matched", matchingRules.contains(simpleEventTypeRule));
        assertFalse("Complex rule should NOT be matched", matchingRules.contains(complexEventTypeRule));
        assertTrue("No event type rule should be matched", matchingRules.contains(noEventTypeRule));

        Event loginEvent = new Event(UUID.randomUUID().toString(), "login", session, profile, TEST_SCOPE, null, null, new Date());
        matchingRules = rulesService.getMatchingRules(loginEvent);
        assertTrue("Complex rule should be matched", matchingRules.contains(complexEventTypeRule));
        assertFalse("Simple rule should NOT be matched", matchingRules.contains(simpleEventTypeRule));

        rulesService.removeRule(simpleEventTypeRule.getItemId());
        rulesService.removeRule(complexEventTypeRule.getItemId());
        rulesService.removeRule(noEventTypeRule.getItemId());
        refreshPersistence();
        rulesService.refreshRules();
    }

    @Test
    public void testRuleOptimizationPerf() throws NoSuchFieldException, IllegalAccessException, IOException, InterruptedException {
        Profile profile = new Profile(UUID.randomUUID().toString());
        Session session = new Session(UUID.randomUUID().toString(), profile, new Date(), TEST_SCOPE);

        updateConfiguration(RulesService.class.getName(), "org.apache.unomi.services", "rules.optimizationActivated", "false");
        rulesService = getService(RulesService.class);
        eventService = getService(EventService.class);

        LOGGER.info("Running unoptimized rules performance test...");
        long unoptimizedRunTime = runEventTest(profile, session);

        updateConfiguration(RulesService.class.getName(), "org.apache.unomi.services", "rules.optimizationActivated", "true");
        rulesService = getService(RulesService.class);
        eventService = getService(EventService.class);

        LOGGER.info("Running optimized rules performance test...");
        long optimizedRunTime = runEventTest(profile, session);

        double improvementRatio = ((double) unoptimizedRunTime) / ((double) optimizedRunTime);
        LOGGER.info("Unoptimized run time = {}ms, optimized run time = {}ms. Improvement={}x", unoptimizedRunTime, optimizedRunTime, improvementRatio);
        // we check with a ratio of 0.9 because the test can sometimes fail due to the fact that the sample size is small and can be affected by
        // environmental issues such as CPU or I/O load.
        assertTrue("Optimized run time should be smaller than unoptimized", improvementRatio > 0.9);
    }

    private long runEventTest(Profile profile, Session session) {
        LOGGER.info("eventService={}", eventService);
        Event viewEvent = generateViewEvent(session, profile);
        int loopCount = 0;
        long startTime = System.currentTimeMillis();
        while (loopCount < 500) {
            eventService.send(viewEvent);
            viewEvent = generateViewEvent(session, profile);
            loopCount++;
        }
        return System.currentTimeMillis() - startTime;
    }

    private Event generateViewEvent(Session session, Profile profile) {
        CustomItem sourceItem = new CustomItem();
        sourceItem.setScope(TEST_SCOPE);

        CustomItem targetItem = new CustomItem();
        targetItem.setScope(TEST_SCOPE);
        Map<String,Object> targetProperties = new HashMap<>();

        Map<String,Object> pageInfo = new HashMap<>();
        pageInfo.put("language", "en");
        pageInfo.put("destinationURL", "https://www.acme.com/test-page.html");
        pageInfo.put("referringURL", "https://unomi.apache.org");
        pageInfo.put("pageID", "ITEM_ID_PAGE");
        pageInfo.put("pagePath", "/test-page.html");
        pageInfo.put("pageName", "Test page");

        targetProperties.put("pageInfo", pageInfo);

        targetItem.setProperties(targetProperties);
        return new Event(UUID.randomUUID().toString(), "view", session, profile, TEST_SCOPE, sourceItem, targetItem, new Date());
    }

    @Test
    public void testGetTrackedConditions() throws InterruptedException, IOException {
        // Add custom condition with parameter
        try {
            ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue(
                    new File("data/tmp/testClickEventCondition.json").toURI().toURL(), ConditionType.class);
            definitionsService.setConditionType(conditionType);
            refreshPersistence();
            rulesService.refreshRules();
            // Test tracked parameter
            // Add rule that has a trackParameter condition that matches
            ConditionBuilder builder = new ConditionBuilder(definitionsService);
            Rule trackParameterRule = new Rule(new Metadata(TEST_SCOPE, "tracked-parameter-rule", "Tracked parameter rule", "A rule with tracked parameter"));
            Condition trackedCondition = builder.condition("clickEventCondition").build();
            trackedCondition.setParameter("path", "/test-page.html");
            trackedCondition.setParameter("referrer", "https://unomi.apache.org");
            trackedCondition.getConditionType().getMetadata().getSystemTags().add("trackedCondition");
            trackParameterRule.setCondition(trackedCondition);
            createAndWaitForRule(trackParameterRule);
            // Add rule that has a trackParameter condition that does not match
            Rule unTrackParameterRule = new Rule(new Metadata(TEST_SCOPE, "not-tracked-parameter-rule", "Not Tracked parameter rule", "A rule that has a parameter not tracked"));
            Condition unTrackedCondition = builder.condition("clickEventCondition").build();
            unTrackedCondition.setParameter("path", "/test-page.html");
            unTrackedCondition.setParameter("referrer", "https://localhost");
            unTrackedCondition.getConditionType().getMetadata().getSystemTags().add("trackedCondition");
            unTrackParameterRule.setCondition(unTrackedCondition);
            createAndWaitForRule(unTrackParameterRule);
            // Check that the given event return the tracked condition
            Profile profile = new Profile(UUID.randomUUID().toString());
            Session session = new Session(UUID.randomUUID().toString(), profile, new Date(), TEST_SCOPE);
            Event viewEvent = generateViewEvent(session, profile);
            Set<Condition> trackedConditions = rulesService.getTrackedConditions(viewEvent.getTarget());
            Assert.assertTrue(trackedConditions.contains(trackedCondition));
            Assert.assertFalse(trackedConditions.contains(unTrackedCondition));
        } finally {
            // Clean up test data
            rulesService.removeRule("tracked-parameter-rule");
            rulesService.removeRule("not-tracked-parameter-rule");
            definitionsService.removeConditionType("clickEventCondition");
        }
    }

    @Override
    public void updateServices() throws InterruptedException {
        super.updateServices();
        rulesService = getService(RulesService.class);
        eventService = getService(EventService.class);
    }
}
