/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2016 ForgeRock AS.
 *
 * Portions Copyrighted 2019 OGIS-RI Co., Ltd.
 */

package com.sun.identity.entitlement.xacml3;

import static com.sun.identity.entitlement.xacml3.FactoryMethods.createArbitraryPrivilege;
import static com.sun.identity.entitlement.xacml3.XACMLExportImport.PrivilegeManagerFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.openam.utils.Time.getCalendarInstance;
import static org.mockito.BDDMockito.*;
import static org.mockito.BDDMockito.mock;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.AssertJUnit.fail;

import java.io.InputStream;
import java.util.Collections;
import java.util.List;

import javax.security.auth.Subject;

import org.forgerock.openam.entitlement.ResourceType;
import org.forgerock.openam.entitlement.service.ApplicationService;
import org.forgerock.openam.entitlement.service.ApplicationServiceFactory;
import org.forgerock.openam.entitlement.service.ResourceTypeService;
import org.forgerock.openam.xacml.v3.DiffStatus;
import org.forgerock.openam.xacml.v3.ImportStep;
import org.forgerock.openam.xacml.v3.PersistableImportStep;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.sun.identity.entitlement.Application;
import com.sun.identity.entitlement.EntitlementException;
import com.sun.identity.entitlement.IPrivilege;
import com.sun.identity.entitlement.Privilege;
import com.sun.identity.entitlement.PrivilegeManager;
import com.sun.identity.entitlement.ReferralPrivilege;
import com.sun.identity.entitlement.ResourceMatch;
import com.sun.identity.entitlement.URLResourceName;
import com.sun.identity.entitlement.xacml3.validation.PrivilegeValidator;
import com.sun.identity.shared.debug.Debug;

public class XACMLExportImportTest {

    private static final String ROOT_REALM = "/";
    private static final Subject NULL_SUBJECT = null;
    private static final InputStream NULL_INPUT = null;
    private final long now = getCalendarInstance().getTimeInMillis();

    private SearchFilterFactory searchFilterFactory;
    private PrivilegeValidator validator;
    private PrivilegeManager pm;
    private XACMLReaderWriter xacmlReaderWriter;
    private PrivilegeManagerFactory pmFactory;
    private Debug debug;
    private ApplicationService applicationService;
    private ApplicationServiceFactory applicationServiceFactory;
    private ResourceTypeService resourceTypeService;

    private XACMLExportImport xacmlExportImport;

    @BeforeMethod
    public void setUp() throws EntitlementException {
        // Constructor Dependencies

        pmFactory = mock(PrivilegeManagerFactory.class);
        pm = mock(PrivilegeManager.class);

        xacmlReaderWriter = mock(XACMLReaderWriter.class);
        validator = mock(PrivilegeValidator.class);
        searchFilterFactory = new SearchFilterFactory();
        debug = mock(Debug.class);
        applicationServiceFactory = mock(ApplicationServiceFactory.class);
        applicationService = mock(ApplicationService.class);
        resourceTypeService = mock(ResourceTypeService.class);
        when(applicationServiceFactory.create(any(Subject.class), nullable(String.class))).thenReturn(applicationService);

        Application application = mock(Application.class);
        URLResourceName urlResourceName = mock(URLResourceName.class);
        ResourceType resourceType = ResourceType.builder()
                .setName("TestResourceType")
                .setUUID("123")
                .setPatterns(Collections.singleton("*://*:*/*")).build();

        // Class under test

        xacmlExportImport = new XACMLExportImport(pmFactory, xacmlReaderWriter, validator, searchFilterFactory, debug,
                applicationServiceFactory, resourceTypeService);

        // Given (shared test state)

        given(pmFactory.createReferralPrivilegeManager(eq(ROOT_REALM), nullable(Subject.class))).willReturn(pm);
        given(applicationServiceFactory.create(nullable(Subject.class), nullable(String.class))).willReturn(applicationService);
        given(applicationService.getApplication(nullable(String.class))).willReturn(application);
        given(application.getResourceComparator()).willReturn(urlResourceName);
        given(urlResourceName.compare(nullable(String.class), nullable(String.class), anyBoolean())).willReturn(ResourceMatch.EXACT_MATCH);
        given(application.getResourceTypeUuids()).willReturn(Collections.singleton("123"));
        given(resourceTypeService.getResourceType(nullable(Subject.class), nullable(String.class), nullable(String.class))).willReturn(resourceType);
    }

    @Test
    public void canImportPrivilegesIntoRealm() throws Exception {
        // Given
        // shared test state
        Privilege privilegeToUpdate = existing(valid(privilege("p1")));
        Privilege privilegeToAdd = notExisting(valid(privilege("p2")));

        PrivilegeSet privilegeSet = new PrivilegeSet();
        privilegeSet.addPrivilege(privilegeToUpdate);
        privilegeSet.addPrivilege(privilegeToAdd);

        given(xacmlReaderWriter.read(eq(NULL_INPUT))).willReturn(privilegeSet);

        // When
        List<ImportStep> importSteps = xacmlExportImport.importXacml(ROOT_REALM, NULL_INPUT, NULL_SUBJECT, false);

        // Then
        assertThat(importSteps).hasSize(2);
        assertImportStep(importSteps.get(0), DiffStatus.UPDATE, privilegeToUpdate);
        assertImportStep(importSteps.get(1), DiffStatus.ADD, privilegeToAdd);

        verify(validator).validatePrivilege(privilegeToAdd);
        verify(validator).validatePrivilege(privilegeToUpdate);

        verify(pm).add(privilegeToAdd);
        verify(pm).modify(privilegeToUpdate);
    }

    @Test
    public void canPerformAnImportDryRun() throws Exception {
        // Given
        // shared test state
        Privilege privilegeToUpdate = existing(valid(privilege("p1")));
        Privilege privilegeToAdd = notExisting(valid(privilege("p2")));

        PrivilegeSet privilegeSet = new PrivilegeSet();
        privilegeSet.addPrivilege(privilegeToUpdate);
        privilegeSet.addPrivilege(privilegeToAdd);

        given(xacmlReaderWriter.read(eq(NULL_INPUT))).willReturn(privilegeSet);

        // When
        List<ImportStep> importSteps = xacmlExportImport.importXacml(ROOT_REALM, NULL_INPUT, NULL_SUBJECT, true);

        // Then
        assertThat(importSteps).hasSize(2);
        assertImportStep(importSteps.get(0), DiffStatus.UPDATE, privilegeToUpdate);
        assertImportStep(importSteps.get(1), DiffStatus.ADD, privilegeToAdd);

        verify(validator).validatePrivilege(privilegeToAdd);
        verify(validator).validatePrivilege(privilegeToUpdate);

        verify(pm, times(0)).add(nullable(Privilege.class));
        verify(pm, times(0)).modify(nullable(Privilege.class));
    }

    @Test(expectedExceptions = EntitlementException.class)
    public void throwsAnExceptionIfPrivilegeValidationFails() throws EntitlementException {
        // Given
        // shared test state
        Privilege invalidPrivilege = invalid(privilege("p1"));
        PrivilegeSet set = new PrivilegeSet();
        set.addPrivilege(invalidPrivilege);

        given(xacmlReaderWriter.read(eq(NULL_INPUT))).willReturn(set);

        // When
        xacmlExportImport.importXacml(ROOT_REALM, NULL_INPUT, NULL_SUBJECT, true);

        // Then
        fail("Expected validation exception");
    }

    private Privilege privilege(String name) throws EntitlementException {
        return createArbitraryPrivilege(name, now);
    }

    private <T extends IPrivilege> T valid(T privilege) {
        return privilege;
    }

    private <T extends IPrivilege> T invalid(T privilege) throws EntitlementException {
        if (privilege instanceof Privilege) {
            doThrow(new EntitlementException(EntitlementException.INVALID_SEARCH_FILTER))
                    .when(validator).validatePrivilege(nullable(Privilege.class));
        } else {
            doThrow(new EntitlementException(EntitlementException.INVALID_SEARCH_FILTER))
                    .when(validator).validateReferralPrivilege(nullable(ReferralPrivilege.class));
        }
        return privilege;
    }

    private <T extends IPrivilege> T existing(T privilege) throws EntitlementException {
        if (privilege instanceof Privilege) {
            given(pm.canFindByName(eq(privilege.getName()))).willReturn(true);
        }
        return privilege;
    }

    private <T extends IPrivilege> T notExisting(T privilege) throws EntitlementException {
        if (privilege instanceof Privilege) {
            given(pm.canFindByName(eq(privilege.getName()))).willReturn(false);
        }
        return privilege;
    }

    public static void assertImportStep(ImportStep importStep, DiffStatus diffStatus, IPrivilege privilege) {
        assertThat(((PersistableImportStep<Privilege>)importStep).get()).isEqualTo(privilege).as("privilege");
        assertThat(importStep.getDiffStatus()).isEqualTo(diffStatus).as("diffStatus");
    }

}
