/**
 * Copyright (c) 2015 Bosch Software Innovations GmbH and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.eclipse.hawkbit.repository.jpa;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.ConstraintViolationException;

import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTagUpdatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetTagUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.Tag;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetTag;
import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.junit.Test;

import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Step;
import io.qameta.allure.Story;

/**
 * Test class for {@link TargetTagManagement}.
 *
 */
@Feature("Component Tests - Repository")
@Story("Target Tag Management")
public class TargetTagManagementTest extends AbstractJpaIntegrationTest {

    @Test
    @Description("Verifies that management get access reacts as specfied on calls for non existing entities by means "
            + "of Optional not present.")
    @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) })
    public void nonExistingEntityAccessReturnsNotPresent() {
        assertThat(targetTagManagement.getByName(NOT_EXIST_ID)).isNotPresent();
        assertThat(targetTagManagement.get(NOT_EXIST_IDL)).isNotPresent();
    }

    @Test
    @Description("Verifies that management queries react as specfied on calls for non existing entities "
            + " by means of throwing EntityNotFoundException.")
    @ExpectEvents({ @Expect(type = DistributionSetTagUpdatedEvent.class, count = 0),
            @Expect(type = TargetTagUpdatedEvent.class, count = 0) })
    public void entityQueriesReferringToNotExistingEntitiesThrowsException() {
        verifyThrownExceptionBy(() -> targetTagManagement.delete(NOT_EXIST_ID), "TargetTag");

        verifyThrownExceptionBy(() -> targetTagManagement.update(entityFactory.tag().update(NOT_EXIST_IDL)),
                "TargetTag");

        verifyThrownExceptionBy(() -> targetTagManagement.findByTarget(PAGE, NOT_EXIST_ID), "Target");
    }

    @Test
    @Description("Verify that a tag with with invalid properties cannot be created or updated")
    public void createAndUpdateTagWithInvalidFields() {
        final TargetTag tag = targetTagManagement
                .create(entityFactory.tag().create().name("tag1").description("tagdesc1"));

        createAndUpdateTagWithInvalidDescription(tag);
        createAndUpdateTagWithInvalidColour(tag);
        createAndUpdateTagWithInvalidName(tag);
    }

    @Step
    private void createAndUpdateTagWithInvalidDescription(final Tag tag) {

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement.create(
                        entityFactory.tag().create().name("a").description(RandomStringUtils.randomAlphanumeric(513))))
                .as("tag with too long description should not be created");

        assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
                () -> targetTagManagement.create(entityFactory.tag().create().name("a").description(INVALID_TEXT_HTML)))
                .as("tag with invalid description should not be created");

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement.update(
                        entityFactory.tag().update(tag.getId()).description(RandomStringUtils.randomAlphanumeric(513))))
                .as("tag with too long description should not be updated");

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement
                        .update(entityFactory.tag().update(tag.getId()).description(INVALID_TEXT_HTML)))
                .as("tag with invalid description should not be updated");
    }

    @Step
    private void createAndUpdateTagWithInvalidColour(final Tag tag) {

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement.create(
                        entityFactory.tag().create().name("a").colour(RandomStringUtils.randomAlphanumeric(17))))
                .as("tag with too long colour should not be created");

        assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
                () -> targetTagManagement.create(entityFactory.tag().create().name("a").colour(INVALID_TEXT_HTML)))
                .as("tag with invalid colour should not be created");

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement.update(
                        entityFactory.tag().update(tag.getId()).colour(RandomStringUtils.randomAlphanumeric(17))))
                .as("tag with too long colour should not be updated");

        assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
                () -> targetTagManagement.update(entityFactory.tag().update(tag.getId()).colour(INVALID_TEXT_HTML)))
                .as("tag with invalid colour should not be updated");
    }

    @Step
    private void createAndUpdateTagWithInvalidName(final Tag tag) {

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement
                        .create(entityFactory.tag().create().name(RandomStringUtils.randomAlphanumeric(
                                NamedEntity.NAME_MAX_SIZE + 1))))
                .as("tag with too long name should not be created");

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement.create(entityFactory.tag().create().name(INVALID_TEXT_HTML)))
                .as("tag with invalidname should not be created");

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement
                        .update(entityFactory.tag().update(tag.getId()).name(RandomStringUtils.randomAlphanumeric(
                                NamedEntity.NAME_MAX_SIZE + 1))))
                .as("tag with too long name should not be updated");

        assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
                () -> targetTagManagement.update(entityFactory.tag().update(tag.getId()).name(INVALID_TEXT_HTML)))
                .as("tag with invalid name should not be updated");

        assertThatExceptionOfType(ConstraintViolationException.class)
                .isThrownBy(() -> targetTagManagement.update(entityFactory.tag().update(tag.getId()).name("")))
                .as("tag with too short name should not be updated");

    }

    @Test
    @Description("Verifies the toogle mechanism by means on assigning tag if at least on target in the list does not have"
            + "the tag yet. Unassign if all of them have the tag already.")
    public void assignAndUnassignTargetTags() {
        final List<Target> groupA = testdataFactory.createTargets(20);
        final List<Target> groupB = testdataFactory.createTargets(20, "groupb", "groupb");

        final TargetTag tag = targetTagManagement
                .create(entityFactory.tag().create().name("tag1").description("tagdesc1"));

        // toggle A only -> A is now assigned
        TargetTagAssignmentResult result = toggleTagAssignment(groupA, tag);
        assertThat(result.getAlreadyAssigned()).isEqualTo(0);
        assertThat(result.getAssigned()).isEqualTo(20);
        assertThat(result.getAssignedEntity()).containsAll(targetManagement
                .getByControllerID(groupA.stream().map(Target::getControllerId).collect(Collectors.toList())));
        assertThat(result.getUnassigned()).isEqualTo(0);
        assertThat(result.getUnassignedEntity()).isEmpty();
        assertThat(result.getTargetTag()).isEqualTo(tag);

        // toggle A+B -> A is still assigned and B is assigned as well
        result = toggleTagAssignment(concat(groupA, groupB), tag);
        assertThat(result.getAlreadyAssigned()).isEqualTo(20);
        assertThat(result.getAssigned()).isEqualTo(20);
        assertThat(result.getAssignedEntity()).containsAll(targetManagement
                .getByControllerID(groupB.stream().map(Target::getControllerId).collect(Collectors.toList())));
        assertThat(result.getUnassigned()).isEqualTo(0);
        assertThat(result.getUnassignedEntity()).isEmpty();
        assertThat(result.getTargetTag()).isEqualTo(tag);

        // toggle A+B -> both unassigned
        result = toggleTagAssignment(concat(groupA, groupB), tag);
        assertThat(result.getAlreadyAssigned()).isEqualTo(0);
        assertThat(result.getAssigned()).isEqualTo(0);
        assertThat(result.getAssignedEntity()).isEmpty();
        assertThat(result.getUnassigned()).isEqualTo(40);
        assertThat(result.getUnassignedEntity()).containsAll(targetManagement.getByControllerID(
                concat(groupB, groupA).stream().map(Target::getControllerId).collect(Collectors.toList())));
        assertThat(result.getTargetTag()).isEqualTo(tag);

    }

    @SafeVarargs
    private final <T> Collection<T> concat(final Collection<T>... targets) {
        final List<T> result = new ArrayList<>();
        Arrays.asList(targets).forEach(result::addAll);
        return result;
    }

    @Test
    @Description("Ensures that all tags are retrieved through repository.")
    public void findAllTargetTags() {
        final List<JpaTargetTag> tags = createTargetsWithTags();

        assertThat(targetTagRepository.findAll()).isEqualTo(targetTagRepository.findAll()).isEqualTo(tags)
                .as("Wrong tag size").hasSize(20);
    }

    @Test
    @Description("Ensures that a created tag is persisted in the repository as defined.")
    public void createTargetTag() {
        final Tag tag = targetTagManagement
                .create(entityFactory.tag().create().name("kai1").description("kai2").colour("colour"));

        assertThat(targetTagRepository.findByNameEquals("kai1").get().getDescription()).as("wrong tag ed")
                .isEqualTo("kai2");
        assertThat(targetTagManagement.getByName("kai1").get().getColour()).as("wrong tag found").isEqualTo("colour");
        assertThat(targetTagManagement.get(tag.getId()).get().getColour()).as("wrong tag found").isEqualTo("colour");
    }

    @Test
    @Description("Ensures that a deleted tag is removed from the repository as defined.")
    public void deleteTargetTags() {

        // create test data
        final Iterable<JpaTargetTag> tags = createTargetsWithTags();
        final TargetTag toDelete = tags.iterator().next();

        for (final Target target : targetRepository.findAll()) {
            assertThat(targetTagManagement.findByTarget(PAGE, target.getControllerId()).getContent())
                    .contains(toDelete);
        }

        // delete
        targetTagManagement.delete(toDelete.getName());

        // check
        for (final Target target : targetRepository.findAll()) {
            assertThat(targetTagManagement.findByTarget(PAGE, target.getControllerId()).getContent())
                    .doesNotContain(toDelete);
        }
        assertThat(targetTagRepository.findById(toDelete.getId())).as("No tag should be found").isNotPresent();
        assertThat(targetTagRepository.findAll()).as("Wrong target tag size").hasSize(19);
    }

    @Test
    @Description("Tests the name update of a target tag.")
    public void updateTargetTag() {
        final List<JpaTargetTag> tags = createTargetsWithTags();

        // change data
        final TargetTag savedAssigned = tags.iterator().next();

        // persist
        targetTagManagement.update(entityFactory.tag().update(savedAssigned.getId()).name("test123"));

        // check data
        assertThat(targetTagRepository.findAll()).as("Wrong target tag size").hasSize(tags.size());
        assertThat(targetTagRepository.findById(savedAssigned.getId()).get().getName()).as("wrong target tag is saved")
                .isEqualTo("test123");
        assertThat(targetTagRepository.findById(savedAssigned.getId()).get().getOptLockRevision())
                .as("wrong target tag is saved").isEqualTo(2);
    }

    @Test
    @Description("Ensures that a tag cannot be created if one exists already with that name (ecpects EntityAlreadyExistsException).")
    public void failedDuplicateTargetTagNameException() {
        targetTagManagement.create(entityFactory.tag().create().name("A"));

        try {
            targetTagManagement.create(entityFactory.tag().create().name("A"));
            fail("should not have worked as tag already exists");
        } catch (final EntityAlreadyExistsException e) {

        }
    }

    @Test
    @Description("Ensures that a tag cannot be updated to a name that already exists on another tag (ecpects EntityAlreadyExistsException).")
    public void failedDuplicateTargetTagNameExceptionAfterUpdate() {
        targetTagManagement.create(entityFactory.tag().create().name("A"));
        final TargetTag tag = targetTagManagement.create(entityFactory.tag().create().name("B"));

        try {
            targetTagManagement.update(entityFactory.tag().update(tag.getId()).name("A"));
            fail("should not have worked as tag already exists");
        } catch (final EntityAlreadyExistsException e) {

        }
    }

    private List<JpaTargetTag> createTargetsWithTags() {
        final List<Target> targets = testdataFactory.createTargets(20);
        final Iterable<TargetTag> tags = testdataFactory.createTargetTags(20, "");

        tags.forEach(tag -> toggleTagAssignment(targets, tag));

        return targetTagRepository.findAll();
    }
}