/*
 * 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.syncope.fit.core;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.security.AccessControlException;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.syncope.client.lib.SyncopeClient;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.common.lib.audit.AuditEntry;
import org.apache.syncope.common.lib.request.UserCR;
import org.apache.syncope.common.lib.to.AuditConfTO;
import org.apache.syncope.common.lib.to.DelegationTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.AuditElements;
import org.apache.syncope.common.lib.types.AuditLoggerName;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.rest.api.beans.AnyQuery;
import org.apache.syncope.common.rest.api.beans.AuditQuery;
import org.apache.syncope.common.rest.api.service.DelegationService;
import org.apache.syncope.common.rest.api.service.UserService;
import org.apache.syncope.core.logic.UserLogic;
import org.apache.syncope.fit.AbstractITCase;
import org.junit.jupiter.api.Test;

public class DelegationITCase extends AbstractITCase {

    private DelegationTO create(final DelegationService ds, final DelegationTO delegation) {
        Response response = ds.create(delegation);
        if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
            Exception ex = CLIENT_FACTORY.getExceptionMapper().fromResponse(response);
            if (ex != null) {
                throw (RuntimeException) ex;
            }
        }
        return getObject(response.getLocation(), DelegationService.class, DelegationTO.class);
    }

    @Test
    public void crudAsAdmin() {
        // 1. create users        
        UserCR delegatingCR = UserITCase.getUniqueSample("delegating@syncope.apache.org");
        delegatingCR.getRoles().add("User reviewer");
        UserTO delegating = createUser(delegatingCR).getEntity();
        assertNotNull(delegating.getKey());

        UserCR delegatedCR = UserITCase.getUniqueSample("delegated@syncope.apache.org");
        UserTO delegated = createUser(delegatedCR).getEntity();
        assertNotNull(delegated.getKey());

        DelegationTO delegation = new DelegationTO();
        delegation.setDelegating(delegating.getKey());
        delegation.setDelegated(delegated.getKey());

        // no dates set -> FAIL
        try {
            DELEGATION_SERVICE.create(delegation);
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.InvalidEntity, e.getType());
        }

        delegation.setStart(OffsetDateTime.now());
        delegation.setEnd(OffsetDateTime.now().minusSeconds(1));

        // end before start -> FAIL
        try {
            DELEGATION_SERVICE.create(delegation);
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.InvalidEntity, e.getType());
        }

        delegation.setEnd(OffsetDateTime.now());

        // 2. create delegation
        delegation = create(DELEGATION_SERVICE, delegation);
        assertNotNull(delegation.getKey());
        assertNotNull(delegation.getEnd());

        // 3. verify delegation is reported for users
        delegating = USER_SERVICE.read(delegating.getKey());
        assertEquals(List.of(delegation.getKey()), delegating.getDelegatingDelegations());
        assertEquals(List.of(), delegating.getDelegatedDelegations());

        delegated = USER_SERVICE.read(delegated.getKey());
        assertEquals(List.of(), delegated.getDelegatingDelegations());
        assertEquals(List.of(delegation.getKey()), delegated.getDelegatedDelegations());

        // 4. update and read delegation
        delegation.setEnd(null);
        DELEGATION_SERVICE.update(delegation);

        delegation = DELEGATION_SERVICE.read(delegation.getKey());
        assertNull(delegation.getEnd());

        // 5. delete delegation
        DELEGATION_SERVICE.delete(delegation.getKey());

        try {
            DELEGATION_SERVICE.read(delegation.getKey());
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.NotFound, e.getType());
        }

        // 6. verify delegation is not reported for users
        delegating = USER_SERVICE.read(delegating.getKey());
        assertEquals(List.of(), delegating.getDelegatingDelegations());
        assertEquals(List.of(), delegating.getDelegatedDelegations());

        delegated = USER_SERVICE.read(delegated.getKey());
        assertEquals(List.of(), delegated.getDelegatingDelegations());
        assertEquals(List.of(), delegated.getDelegatedDelegations());
    }

    @Test
    public void crudAsUser() {
        // 1. create users        
        UserCR delegatingCR = UserITCase.getUniqueSample("delegating@syncope.apache.org");
        delegatingCR.getRoles().add("User reviewer");
        UserTO delegating = createUser(delegatingCR).getEntity();
        assertNotNull(delegating.getKey());

        UserCR delegatedCR = UserITCase.getUniqueSample("delegated@syncope.apache.org");
        UserTO delegated = createUser(delegatedCR).getEntity();
        assertNotNull(delegated.getKey());

        DelegationTO delegation = new DelegationTO();
        delegation.setDelegating("c9b2dec2-00a7-4855-97c0-d854842b4b24");
        delegation.setDelegated(delegated.getKey());
        delegation.setStart(OffsetDateTime.now());

        DelegationService uds = CLIENT_FACTORY.create(delegating.getUsername(), "password123").
                getService(DelegationService.class);

        // delegating user is not requesting user -> FAIL
        try {
            create(uds, delegation);
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
        }

        // 2. create delegation
        delegation.setDelegating(delegating.getKey());

        delegation = create(uds, delegation);
        assertNotNull(delegation.getKey());
        assertNull(delegation.getEnd());

        // 3. update and read delegation
        delegation.setEnd(OffsetDateTime.now());
        uds.update(delegation);

        delegation = uds.read(delegation.getKey());
        assertNotNull(delegation.getEnd());

        // 4. delete delegation
        uds.delete(delegation.getKey());

        try {
            uds.read(delegation.getKey());
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.NotFound, e.getType());
        }
    }

    @Test
    public void operations() {
        // 0. enable audit
        AuditLoggerName authLoginSuccess = new AuditLoggerName(
                AuditElements.EventCategoryType.LOGIC,
                UserLogic.class.getSimpleName(),
                null,
                "search",
                AuditElements.Result.SUCCESS);
        AuditConfTO authLogin = new AuditConfTO();
        authLogin.setKey(authLoginSuccess.toAuditKey());
        authLogin.setActive(true);
        AUDIT_SERVICE.set(authLogin);

        // 1. bellini delegates rossini
        DelegationTO delegation = new DelegationTO();
        delegation.setDelegating("bellini");
        delegation.setDelegated("rossini");
        delegation.setStart(OffsetDateTime.now());
        delegation = create(DELEGATION_SERVICE, delegation);
        assertNotNull(delegation.getKey());

        // 2. search users as bellini
        SyncopeClient bellini = CLIENT_FACTORY.create("bellini", "password");
        int forBellini = bellini.getService(UserService.class).search(
                new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).build()).getTotalCount();

        SyncopeClient rossini = CLIENT_FACTORY.create("rossini", "password");

        // 3. search users as rossini
        Triple<Map<String, Set<String>>, List<String>, UserTO> self = rossini.self();
        assertEquals(List.of("bellini"), self.getMiddle());

        // 3a. search users as rossini without delegation -> FAIL
        try {
            rossini.getService(UserService.class).search(
                    new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).build());
            fail();
        } catch (ForbiddenException e) {
            assertNotNull(e);
        }

        // 3b. search users as rossini with delegation -> SUCCESS
        int forRossini = rossini.delegatedBy("bellini").getService(UserService.class).search(
                new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).build()).getTotalCount();
        if (!IS_ELASTICSEARCH_ENABLED) {
            assertEquals(forBellini, forRossini);
        }

        // 4. delete delegation: searching users as rossini does not work, even with delegation
        DELEGATION_SERVICE.delete(delegation.getKey());

        try {
            rossini.getService(UserService.class).search(
                    new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).build());
            fail();
        } catch (AccessControlException e) {
            assertNotNull(e);
        }

        // 5. query audit entries
        AuditQuery query = new AuditQuery.Builder().
                type(authLoginSuccess.getType()).
                category(authLoginSuccess.getCategory()).
                event(authLoginSuccess.getEvent()).
                result(authLoginSuccess.getResult()).
                build();
        List<AuditEntry> entries = query(query, MAX_WAIT_SECONDS);
        assertTrue(entries.stream().anyMatch(entry -> "rossini [delegated by bellini]".equals(entry.getWho())));

        // 6. disable audit
        authLogin.setActive(false);
        AUDIT_SERVICE.set(authLogin);
    }
}
