/*
 * 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.CustomItem;
import org.apache.unomi.api.Event;
import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.Profile;
import org.apache.unomi.api.actions.Action;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.rules.Rule;
import org.apache.unomi.api.services.DefinitionsService;
import org.apache.unomi.api.services.EventService;
import org.apache.unomi.api.services.ProfileService;
import org.apache.unomi.api.services.RulesService;
import org.junit.After;
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 javax.inject.Inject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

import static org.apache.unomi.itests.BasicIT.ITEM_TYPE_PAGE;

@RunWith(PaxExam.class)
@ExamReactorStrategy(PerSuite.class)
public class IncrementPropertyIT extends BaseIT {
    private Profile profile;
    private Rule rule;
    private Event event;

    @Before
    public void setup() throws Exception {
        profile = createProfile();
        rule = new Rule();
    }

    @After
    public void tearDown() throws InterruptedException {
        rulesService.removeRule(rule.getItemId());
        profileService.delete(profile.getItemId(), false);
        waitForNullValue("Rule still present after deletion", () -> rulesService.getRule(rule.getItemId()), DEFAULT_TRYING_TIMEOUT,
                DEFAULT_TRYING_TRIES);
        waitForNullValue("Profile still present after deletion", () -> profileService.load(profile.getItemId()), DEFAULT_TRYING_TIMEOUT,
                DEFAULT_TRYING_TRIES);
    }

    @Test
    public void testIncrementNotExistingPropertyWithDynamicName() throws InterruptedException {
        int eventCode = buildActionAndSendEvent("pageView.${eventProperty::target.scope}", null, null, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme-space");
            Assert.assertEquals(1, value, 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementExistingPropertyWithDynamicName() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("acme-space", 24);
        properties.put("pageView", propertyValue);

        int eventCode = buildActionAndSendEvent("pageView.${eventProperty::target.scope}", null, properties, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme-space");
            Assert.assertEquals(25, value, 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementNotExistingProperty() throws InterruptedException {
        int eventCode = buildActionAndSendEvent("pageView.acme", null, null, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme");
            Assert.assertEquals(1, value, 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementExistingProperty() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("acme", 49);
        properties.put("pageView", propertyValue);

        int eventCode = buildActionAndSendEvent("pageView.acme", null, properties, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme");
            Assert.assertEquals(50, value, 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementExistingPropertyWithExistingEventProperty() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("acme", 49);
        properties.put("pageView", propertyValue);

        Map<String, Object> targetProperties = new HashMap<>();
        propertyValue = new HashMap<>();
        propertyValue.put("nasa", 19);
        targetProperties.put("project", propertyValue);

        int eventCode = buildActionAndSendEvent("pageView.acme", "project.nasa", properties, targetProperties);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme");
            Assert.assertEquals(68, value, 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementNotExistingObjectPropertyWithExistingEventObjectProperty() throws InterruptedException {
        Map<String, Object> targetProperties = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("acme", 49);
        propertyValue.put("health", 18);
        propertyValue.put("sport", 99);
        targetProperties.put("pageView", propertyValue);

        int eventCode = buildActionAndSendEvent("pageView", "pageView", null, targetProperties);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Integer> property = ((Map<String, Integer>) updatedProfile.getProperty("pageView"));
            Assert.assertEquals(49, property.get("acme"), 0.0);
            Assert.assertEquals(18, property.get("health"), 0.0);
            Assert.assertEquals(99, property.get("sport"), 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementExistingObjectProperty() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("acme", 49);
        propertyValue.put("nasa", 5);
        properties.put("pageView", propertyValue);

        int eventCode = buildActionAndSendEvent("pageView", null, properties, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Integer> property = ((Map<String, Integer>) updatedProfile.getProperty("pageView"));
            Assert.assertEquals(50, property.get("acme"), 0.0);
            Assert.assertEquals(6, property.get("nasa"), 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementExistingObjectPropertyWithExistingEventObjectProperty() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("acme", 49);
        properties.put("pageView", propertyValue);

        Map<String, Object> targetProperties = new HashMap<>();
        Map<String, Integer> propertyValue1 = new HashMap<>();
        propertyValue1.put("acme", 31);
        propertyValue1.put("health", 88);
        propertyValue1.put("sport", 9);
        targetProperties.put("pageView", propertyValue1);

        int eventCode = buildActionAndSendEvent("pageView", "pageView", properties, targetProperties);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Integer> property = ((Map<String, Integer>) updatedProfile.getProperty("pageView"));
            Assert.assertEquals(80, property.get("acme"), 0.0);
            Assert.assertEquals(88, property.get("health"), 0.0);
            Assert.assertEquals(9, property.get("sport"), 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementExistingPropertyNested() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Object> properties1 = new HashMap<>();
        Map<String, Object> properties2 = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("city", 13);
        properties2.put("state", propertyValue);
        properties1.put("country", properties2);
        properties.put("continent", properties1);

        int eventCode = buildActionAndSendEvent("continent.country.state.city", null, properties, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Integer> property = (Map<String, Integer>) ((Map<String, Object>) ((Map<String, Object>) updatedProfile
                    .getProperty("continent")).get("country")).get("state");
            Assert.assertEquals(14, property.get("city"), 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementNotExistingPropertyNested() throws InterruptedException {
        int eventCode = buildActionAndSendEvent("continent.country.state.city", null, null, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Integer> property = (Map<String, Integer>) ((Map<String, Object>) ((Map<String, Object>) updatedProfile
                    .getProperty("continent")).get("country")).get("state");
            Assert.assertEquals(1, property.get("city"), 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementExistingPropertyNestedWithExistingEventProperty() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Object> properties1 = new HashMap<>();
        Map<String, Object> properties2 = new HashMap<>();
        Map<String, Integer> propertyValue = new HashMap<>();
        propertyValue.put("city", 13);
        properties2.put("state", propertyValue);
        properties1.put("country", properties2);
        properties.put("continent", properties1);

        Map<String, Object> targetProperties = new HashMap<>();
        Map<String, Object> properties3 = new HashMap<>();
        Map<String, Object> propertyValue1 = new HashMap<>();
        propertyValue1.put("zone", 107);
        properties3.put("mars", propertyValue1);
        targetProperties.put("planet", properties3);

        int eventCode = buildActionAndSendEvent("continent.country.state.city", "planet.mars.zone", properties, targetProperties);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Integer> property = (Map<String, Integer>) ((Map<String, Object>) ((Map<String, Object>) updatedProfile
                    .getProperty("continent")).get("country")).get("state");
            Assert.assertEquals(120, property.get("city"), 0.0);
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementObjectPropertyContainsStringValue() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Object> propertyValue = new HashMap<>();
        propertyValue.put("books", 59);
        propertyValue.put("chapters", 1001);
        propertyValue.put("featured", "The forty rules");
        properties.put("library", propertyValue);

        int eventCode = buildActionAndSendEvent("library", null, properties, null);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Object> property = ((Map<String, Object>) updatedProfile.getProperty("library"));
            Assert.assertEquals(60, (int) property.get("books"), 0.0);
            Assert.assertEquals(1002, (int) property.get("chapters"), 0.0);
            Assert.assertEquals("The forty rules", property.get("featured"));
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    @Test
    public void testIncrementObjectPropertyContainsStringValueWithExistingEventProperty() throws InterruptedException {
        Map<String, Object> properties = new HashMap<>();
        Map<String, Object> propertyValue = new HashMap<>();
        propertyValue.put("books", 59);
        propertyValue.put("chapters", 1001);
        propertyValue.put("featured", "The forty rules");
        properties.put("library", propertyValue);

        Map<String, Object> targetProperties = new HashMap<>();
        Map<String, Object> properties1 = new HashMap<>();
        Map<String, Object> propertyValue1 = new HashMap<>();
        propertyValue1.put("books", 222);
        propertyValue1.put("chapters", 2048);
        propertyValue1.put("featured", "Bible");
        properties1.put("library", propertyValue1);
        targetProperties.put("main", properties1);

        int eventCode = buildActionAndSendEvent("library", "main.library", properties, targetProperties);

        if (eventCode == EventService.PROFILE_UPDATED) {
            Profile updatedProfile = profileService.save(event.getProfile());

            Map<String, Object> property = ((Map<String, Object>) updatedProfile.getProperty("library"));
            Assert.assertEquals(281, (int) property.get("books"), 0.0);
            Assert.assertEquals(3049, (int) property.get("chapters"), 0.0);
            Assert.assertEquals("The forty rules", property.get("featured"));
        } else {
            Assert.fail("Profile was not updated");
        }
    }

    private void createRule(Action incrementPropertyAction) throws InterruptedException {
        Condition condition = createCondition();
        Metadata metadata = createMetadata();

        List<Action> actions = new ArrayList<>();
        actions.add(incrementPropertyAction);

        rule.setCondition(condition);
        rule.setActions(actions);
        rule.setMetadata(metadata);
        createAndWaitForRule(rule);
    }

    private int buildActionAndSendEvent(String propertyName, String propertyTargetName, Map<String, Object> properties,
            Map<String, Object> targetProperties) throws InterruptedException {
        Action incrementPropertyAction = new Action(definitionsService.getActionType("incrementPropertyAction"));
        incrementPropertyAction.setParameter("propertyName", propertyName);
        if (propertyTargetName != null)
            incrementPropertyAction.setParameter("propertyTarget", propertyTargetName);

        createRule(incrementPropertyAction);

        if (properties != null)
            profile.setProperties(properties);

        CustomItem target = new CustomItem("ITEM_ID_PAGE", ITEM_TYPE_PAGE);
        target.setScope("acme-space");
        if (targetProperties == null) {
            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);
        target.setProperties(targetProperties);

        event = new Event("view", null, profile, null, null, target, new Date());
        event.setPersistent(false);

        int eventCode = eventService.send(event);

        return eventCode;
    }

    private Metadata createMetadata() {
        String itemId = UUID.randomUUID().toString();
        Metadata metadata = new Metadata();
        metadata.setId(itemId);
        metadata.setName(itemId);
        metadata.setDescription(itemId);
        metadata.setEnabled(true);
        metadata.setScope("systemscope");
        return metadata;
    }

    private Condition createCondition() {
        Condition condition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
        condition.setParameter("eventTypeId", "view");
        return condition;
    }

    private Profile createProfile() throws InterruptedException {
        Profile profile = new Profile(UUID.randomUUID().toString());

        profileService.save(profile);

        return keepTrying("Profile " + profile.getItemId() + " not found in the required time",
                () -> profileService.load(profile.getItemId()), Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
    }
}
