/**
 * Copyright (c) 2021 Bosch.IO GmbH and others
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.hawkbit.repository.jpa.management;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.Collections;
import java.util.List;

import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation.CancelationType;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidationCount;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.junit.jupiter.api.Test;
import org.springframework.data.repository.query.Param;

/**
 * Test class testing the functionality of invalidating a
 * {@link DistributionSet}
  * <p/>
 * Feature: Component Tests - Repository<br/>
 * Story: Distribution set invalidation management
 */
class DistributionSetInvalidationManagementTest extends AbstractJpaIntegrationTest {

    /**
     * Verify invalidation of distribution sets that only removes distribution sets from auto assignments
     */
    @Test
    void verifyInvalidateDistributionSetStopAutoAssignment() {
        final InvalidationTestData invalidationTestData = createInvalidationTestData("verifyInvalidateDistributionSetStopAutoAssignment");

        final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
                Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE,
                false);
        final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
                .countEntitiesForInvalidation(distributionSetInvalidation);
        assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 0);

        distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation);
        rolloutHandler.handleAll();

        assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get()
                .getAutoAssignDistributionSet()).isNull();
        assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus())
                .isNotIn(RolloutStatus.STOPPING, RolloutStatus.FINISHED);
        for (final Target target : invalidationTestData.getTargets()) {
            // if status is pending, the assignment has not been canceled
            assertThat(targetRepository.findById(target.getId()).get().getUpdateStatus())
                    .isEqualTo(TargetUpdateStatus.PENDING);
            assertThat(findActionsByTarget(target)).hasSize(1);
            assertThat(findActionsByTarget(target).get(0).getStatus()).isEqualTo(Status.RUNNING);
        }
    }

    /**
     * Verify invalidation of distribution sets that removes distribution sets from auto assignments and stops rollouts
     */
    @Test
    void verifyInvalidateDistributionSetStopRollouts() {
        final InvalidationTestData invalidationTestData = createInvalidationTestData(
                "verifyInvalidateDistributionSetStopRollouts");

        final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
                Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE,
                true);
        final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
                .countEntitiesForInvalidation(distributionSetInvalidation);
        assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 0, 1);

        distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation);
        rolloutHandler.handleAll();

        assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get()
                .getAutoAssignDistributionSet()).isNull();
        assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus())
                .isEqualTo(RolloutStatus.FINISHED);
        assertNoScheduledActionsExist(invalidationTestData.getRollout());
        assertRolloutGroupsAreFinished(invalidationTestData.getRollout());
        for (final Target target : invalidationTestData.getTargets()) {
            // if status is pending, the assignment has not been canceled
            assertThat(
                    targetRepository.findById(invalidationTestData.getTargets().get(0).getId()).get().getUpdateStatus())
                    .isEqualTo(TargetUpdateStatus.PENDING);
            assertThat(findActionsByTarget(target)).hasSize(1);
            assertThat(findActionsByTarget(target).get(0).getStatus()).isEqualTo(Status.RUNNING);
        }
    }

    /**
     * Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and force cancels assignments
     */
    @Test
    void verifyInvalidateDistributionSetStopAllAndForceCancel() {
        final InvalidationTestData invalidationTestData = createInvalidationTestData(
                "verifyInvalidateDistributionSetStopAllAndForceCancel");

        final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
                Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.FORCE,
                true);
        final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
                .countEntitiesForInvalidation(distributionSetInvalidation);
        assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1);

        distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation);
        rolloutHandler.handleAll();

        assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get()
                .getAutoAssignDistributionSet()).isNull();
        assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus())
                .isEqualTo(RolloutStatus.FINISHED);
        assertNoScheduledActionsExist(invalidationTestData.getRollout());
        assertRolloutGroupsAreFinished(invalidationTestData.getRollout());
        for (final Target target : invalidationTestData.getTargets()) {
            assertThat(targetRepository.findById(target.getId()).get().getUpdateStatus())
                    .isEqualTo(TargetUpdateStatus.IN_SYNC);
            assertThat(findActionsByTarget(target)).hasSize(1);
            assertThat(findActionsByTarget(target).get(0).getStatus()).isEqualTo(Status.CANCELED);
        }
    }

    /**
     * Verify invalidation of distribution sets that removes distribution sets from auto assignments, stops rollouts and cancels assignments
     */
    @Test
    void verifyInvalidateDistributionSetStopAll() {
        final InvalidationTestData invalidationTestData = createInvalidationTestData(
                "verifyInvalidateDistributionSetStopAll");

        final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
                Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT,
                true);
        final DistributionSetInvalidationCount distributionSetInvalidationCount = distributionSetInvalidationManagement
                .countEntitiesForInvalidation(distributionSetInvalidation);
        assertDistributionSetInvalidationCount(distributionSetInvalidationCount, 1, 5, 1);

        distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation);

        assertThat(targetFilterQueryManagement.get(invalidationTestData.getTargetFilterQuery().getId()).get()
                .getAutoAssignDistributionSet()).isNull();
        assertThat(rolloutRepository.findById(invalidationTestData.getRollout().getId()).get().getStatus())
                .isIn(RolloutStatus.STOPPING, RolloutStatus.FINISHED);
        for (final Target target : invalidationTestData.getTargets()) {
            assertThat(targetRepository.findById(target.getId()).get().getUpdateStatus())
                    .isEqualTo(TargetUpdateStatus.PENDING);
            assertThat(findActionsByTarget(target)).hasSize(1);
            assertThat(findActionsByTarget(target).get(0).getStatus()).isEqualTo(Status.CANCELING);
        }
    }

    /**
     * Verify that invalidating an incomplete distribution set throws an exception
     */
    @Test
    void verifyInvalidateIncompleteDistributionSetThrowsException() {
        final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet();

        final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
                List.of(distributionSet.getId()), CancelationType.SOFT, true);
        assertThatExceptionOfType(IncompleteDistributionSetException.class)
                .as("Incomplete distributionSet should throw an exception")
                .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation));
    }

    /**
     * Verify that invalidating an invalidated distribution set don't throws an exception
     *  -> should be able to cancel actions again (if previous time there was a problem
     */
    @Test
    @SuppressWarnings("java:S2699") // test that no exception is thrown
    void verifyInvalidateInvalidatedDistributionSetDontThrowsException() {
        final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet();
        distributionSetInvalidationManagement.invalidateDistributionSet(
                new DistributionSetInvalidation(Collections.singletonList(distributionSet.getId()),
                        CancelationType.SOFT, true));
    }

    /**
     * Verify that a user that has authority READ_REPOSITORY and UPDATE_REPOSITORY is allowed to invalidate a distribution set
     */
    @Test
    @WithUser(authorities = { "READ_REPOSITORY", "UPDATE_REPOSITORY" })
    void verifyInvalidateWithReadAndUpdateRepoAuthority() {
        final InvalidationTestData invalidationTestData = systemSecurityContext
                .runAsSystem(() -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAuthority"));

        distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation(
                Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE,
                false));
        assertThat(
                distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid())
                .isFalse();
    }

    /**
     * Verify that a user that has authority READ_REPOSITORY, UPDATE_REPOSITORY and UPDATE_TARGET is allowed to invalidate a distribution set only without canceling rollouts
     */
    @Test
    @WithUser(authorities = { "READ_REPOSITORY", "UPDATE_REPOSITORY", "UPDATE_TARGET" })
    void verifyInvalidateWithReadAndUpdateRepoAndUpdateTargetAuthority() {
        final InvalidationTestData invalidationTestData = systemSecurityContext.runAsSystem(
                () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority"));

        final DistributionSetInvalidation distributionSetInvalidation = new DistributionSetInvalidation(
                List.of(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT, true);
        assertThatExceptionOfType(InsufficientPermissionException.class)
                .as("Insufficient permission exception expected")
                .isThrownBy(() -> distributionSetInvalidationManagement.invalidateDistributionSet(distributionSetInvalidation));

        distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation(
                Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.NONE,
                false));
        assertThat(
                distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid())
                .isFalse();
    }

    /**
     * Verify that a user that has authority READ_REPOSITORY, UPDATE_REPOSITORY, UPDATE_ROLLOUT and UPDATE_TARGET is allowed to invalidate a distribution
     */
    @Test
    @WithUser(authorities = { "READ_REPOSITORY", "UPDATE_REPOSITORY", "UPDATE_TARGET", "UPDATE_ROLLOUT" })
    void verifyInvalidateWithReadAndUpdateRepoAndUpdateTargetAndUpdateRolloutAuthority() {
        final InvalidationTestData invalidationTestData = systemSecurityContext.runAsSystem(
                () -> createInvalidationTestData("verifyInvalidateWithUpdateRepoAndUpdateTargetAuthority"));

        distributionSetInvalidationManagement.invalidateDistributionSet(new DistributionSetInvalidation(
                Collections.singletonList(invalidationTestData.getDistributionSet().getId()), CancelationType.SOFT,
                true));
        assertThat(
                distributionSetRepository.findById(invalidationTestData.getDistributionSet().getId()).get().isValid())
                .isFalse();
    }

    private void assertNoScheduledActionsExist(final Rollout rollout) {
        assertThat(
                actionRepository.findByRolloutIdAndStatus(PAGE, rollout.getId(), Status.SCHEDULED).getTotalElements())
                .isZero();
    }

    private void assertRolloutGroupsAreFinished(final Rollout rollout) {
        assertThat(rolloutGroupRepository.findByRolloutId(rollout.getId(), PAGE))
                .isNotEmpty()
                .allMatch(rolloutGroup -> rolloutGroup.getStatus().equals(RolloutGroupStatus.FINISHED));
    }

    private InvalidationTestData createInvalidationTestData(final String testName) {
        final DistributionSet distributionSet = testdataFactory.createDistributionSet();
        final List<Target> targets = testdataFactory.createTargets(5, testName);
        assignDistributionSet(distributionSet, targets);
        final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(entityFactory.targetFilterQuery()
                .create().name(testName).query("name==*").autoAssignDistributionSet(distributionSet));
        final Rollout rollout = testdataFactory.createRolloutByVariables(testName, "desc", 2, "name==*",
                distributionSet, "50", "80");

        return new InvalidationTestData(distributionSet, targets, targetFilterQuery, rollout);
    }

    private void assertDistributionSetInvalidationCount(
            final DistributionSetInvalidationCount distributionSetInvalidationCount,
            final long expectedAutoAssignmentCount, final long expectedActionCount, final long expectedRolloutCount) {
        assertThat(distributionSetInvalidationCount.getAutoAssignmentCount()).isEqualTo(expectedAutoAssignmentCount);
        assertThat(distributionSetInvalidationCount.getActionCount()).isEqualTo(expectedActionCount);
        assertThat(distributionSetInvalidationCount.getRolloutsCount()).isEqualTo(expectedRolloutCount);
    }

    private List<JpaAction> findActionsByTarget(@Param("target") Target target) { // order by id ?
        return actionRepository.findAll(ActionSpecifications.byTargetControllerId(target.getControllerId()));
    }

    private static class InvalidationTestData {

        private final DistributionSet distributionSet;
        private final List<Target> targets;
        private final TargetFilterQuery targetFilterQuery;
        private final Rollout rollout;

        public InvalidationTestData(final DistributionSet distributionSet, final List<Target> targets,
                final TargetFilterQuery targetFilterQuery, final Rollout rollout) {
            super();
            this.distributionSet = distributionSet;
            this.targets = targets;
            this.targetFilterQuery = targetFilterQuery;
            this.rollout = rollout;
        }

        public DistributionSet getDistributionSet() {
            return distributionSet;
        }

        public List<Target> getTargets() {
            return targets;
        }

        public TargetFilterQuery getTargetFilterQuery() {
            return targetFilterQuery;
        }

        public Rollout getRollout() {
            return rollout;
        }
    }
}
