package org.apereo.cas.services;

import com.google.common.base.Throwables;
import org.apache.commons.io.FileUtils;
import org.apereo.cas.authentication.principal.ShibbolethCompatiblePersistentIdGenerator;
import org.apereo.cas.services.support.RegisteredServiceRegexAttributeFilter;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.ClassPathResource;

import java.net.URI;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.junit.Assert.*;

/**
 * This is {@link AbstractResourceBasedServiceRegistryDaoTests}.
 *
 * @author Misagh Moayyed
 * @since 5.0.0
 */
public abstract class AbstractResourceBasedServiceRegistryDaoTests {

    public static final ClassPathResource RESOURCE = new ClassPathResource("services");
    private static final String SERVICE_ID = "testId";
    private static final String THEME = "theme";
    private static final String DESCRIPTION = "description";
    private static final String HTTPS_SERVICE_ID = "^https://.+";

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    protected ServiceRegistryDao dao;

    @BeforeClass
    public static void prepTests() throws Exception {
        FileUtils.cleanDirectory(RESOURCE.getFile());
    }

    @Test
    public void checkLoadingOfServiceFiles() throws Exception {
        prepTests();
        verifySaveAttributeReleasePolicyMappingRules();
        verifySaveAttributeReleasePolicyAllowedAttrRulesAndFilter();
        assertEquals(this.dao.load().size(), 2);
    }

    @Test
    public void checkSaveMethodWithNonExistentServiceAndNoAttributes() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveMethodWithNonExistentServiceAndNoAttributes");
        r.setServiceId(SERVICE_ID);
        r.setTheme(THEME);
        r.setDescription(DESCRIPTION);

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());

        assertEquals(r, r2);
        assertEquals(r2, r3);
    }

    @Test
    public void execSaveWithAuthnMethodPolicy() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("execSaveWithAuthnMethodPolicy");
        r.setServiceId(SERVICE_ID);
        r.setTheme(THEME);
        r.setDescription(DESCRIPTION);

        final DefaultRegisteredServiceMultifactorPolicy policy =
                new DefaultRegisteredServiceMultifactorPolicy();
        policy.setFailureMode(RegisteredServiceMultifactorPolicy.FailureModes.PHANTOM);

        final Set<String> set = new HashSet<>();
        set.add("duoAuthenticationProvider");
        policy.setMultifactorAuthenticationProviders(set);
        policy.setPrincipalAttributeNameTrigger("memberOf");
        policy.setPrincipalAttributeValueToMatch("cas|CAS|admin");
        r.setMultifactorPolicy(policy);
        final RegisteredService r2 = this.dao.save(r);
        assertEquals(r2, r);
    }

    @Test
    public void execSaveMethodWithDefaultUsernameAttribute() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveMethodWithDefaultUsernameAttribute");
        r.setServiceId(SERVICE_ID);
        r.setTheme(THEME);
        r.setDescription(DESCRIPTION);
        r.setUsernameAttributeProvider(new DefaultRegisteredServiceUsernameProvider());
        final RegisteredService r2 = this.dao.save(r);
        assertEquals(r2, r);
    }

    @Test
    public void ensureSaveMethodWithDefaultPrincipalAttribute() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveMethodWithDefaultPrincipalAttribute");
        r.setServiceId(SERVICE_ID);
        r.setTheme(THEME);
        r.setDescription(DESCRIPTION);
        r.setUsernameAttributeProvider(new PrincipalAttributeRegisteredServiceUsernameProvider("cn", "UPPER"));
        final RegisteredService r2 = this.dao.save(r);
        assertEquals(r2, r);
    }

    @Test
    public void verifySaveMethodWithDefaultAnonymousAttribute() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveMethodWithDefaultAnonymousAttribute");
        r.setServiceId(SERVICE_ID);
        r.setTheme(THEME);
        r.setDescription(DESCRIPTION);
        r.setUsernameAttributeProvider(new AnonymousRegisteredServiceUsernameAttributeProvider(
                new ShibbolethCompatiblePersistentIdGenerator("helloworld")
        ));
        final RegisteredService r2 = this.dao.save(r);
        this.dao.load();
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());
        final AnonymousRegisteredServiceUsernameAttributeProvider anon =
                (AnonymousRegisteredServiceUsernameAttributeProvider) r3.getUsernameAttributeProvider();
        final ShibbolethCompatiblePersistentIdGenerator ss =
                (ShibbolethCompatiblePersistentIdGenerator) anon.getPersistentIdGenerator();
        assertEquals(new String(ss.getSalt()), "helloworld");
        assertEquals(r2, r3);
    }

    @Test
    public void verifySaveAttributeReleasePolicy() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveAttributeReleasePolicy");
        r.setServiceId(SERVICE_ID);
        r.setTheme(THEME);
        r.setDescription(DESCRIPTION);
        r.setAttributeReleasePolicy(new ReturnAllAttributeReleasePolicy());

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());

        assertEquals(r, r2);
        assertEquals(r2, r3);
        assertNotNull(r3.getAttributeReleasePolicy());
        assertEquals(r2.getAttributeReleasePolicy(), r3.getAttributeReleasePolicy());
    }

    @Test
    public void verifySaveMethodWithExistingServiceNoAttribute() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveMethodWithExistingServiceNoAttribute");
        r.setServiceId(SERVICE_ID);
        r.setTheme(THEME);
        r.setDescription(DESCRIPTION);
        this.dao.save(r);
        r.setTheme("mytheme");

        this.dao.save(r);

        final RegisteredService r3 = this.dao.findServiceById(r.getId());
        assertEquals(r, r3);
    }

    @Test
    public void verifySaveAttributeReleasePolicyMappingRules() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveAttributeReleasePolicyMappingRules");
        r.setServiceId(SERVICE_ID);

        final Map<String, String> map = new HashMap<>();
        map.put("attr1", "newattr1");
        map.put("attr2", "newattr2");
        map.put("attr2", "newattr3");


        final ReturnMappedAttributeReleasePolicy policy = new ReturnMappedAttributeReleasePolicy();
        policy.setAllowedAttributes(map);
        r.setAttributeReleasePolicy(policy);

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());

        assertEquals(r, r2);
        assertEquals(r2, r3);
        assertNotNull(r3.getAttributeReleasePolicy());
        assertEquals(r2.getAttributeReleasePolicy(), r3.getAttributeReleasePolicy());
    }

    @Test
    public void verifySaveAttributeReleasePolicyAllowedAttrRules() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveAttributeReleasePolicyAllowedAttrRules");
        r.setServiceId(SERVICE_ID);

        final ReturnAllowedAttributeReleasePolicy policy = new ReturnAllowedAttributeReleasePolicy();
        policy.setAllowedAttributes(Arrays.asList("1", "2", "3"));
        r.setAttributeReleasePolicy(policy);

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());

        assertEquals(r, r2);
        assertEquals(r2, r3);
        assertNotNull(r3.getAttributeReleasePolicy());
        assertEquals(r2.getAttributeReleasePolicy(), r3.getAttributeReleasePolicy());
    }

    @Test
    public void verifySaveAttributeReleasePolicyAllowedAttrRulesAndFilter() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setName("testSaveAttributeReleasePolicyAllowedAttrRulesAndFilter");
        r.setServiceId(SERVICE_ID);
        r.setTheme("testtheme");
        r.setEvaluationOrder(1000);
        r.setAccessStrategy(new DefaultRegisteredServiceAccessStrategy(true, false));
        r.setProxyPolicy(new RegexMatchingRegisteredServiceProxyPolicy("https://.+"));
        r.setRequiredHandlers(Stream.of("h1", "h2").collect(Collectors.toSet()));

        final ReturnAllowedAttributeReleasePolicy policy = new ReturnAllowedAttributeReleasePolicy();
        policy.setAllowedAttributes(Arrays.asList("1", "2", "3"));
        r.setAttributeReleasePolicy(policy);
        r.getAttributeReleasePolicy().setAttributeFilter(new RegisteredServiceRegexAttributeFilter("\\w+"));

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());

        assertEquals(r, r2);
        assertEquals(r2, r3);
        assertNotNull(r3.getAttributeReleasePolicy());
        assertEquals(r2.getAttributeReleasePolicy(), r3.getAttributeReleasePolicy());
    }

    @Test
    public void verifyServiceType() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("testServiceType");
        r.setTheme("testtheme");
        r.setEvaluationOrder(1000);

        final RegisteredService r2 = this.dao.save(r);
        assertTrue(r2 instanceof RegexRegisteredService);
    }

    @Test
    public void verifyServiceWithInvalidFileName() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("hell/o@world:*");
        r.setEvaluationOrder(1000);
        this.thrown.expect(IllegalArgumentException.class);
        this.dao.save(r);
    }

    @Test
    public void verifyServiceRemovals() throws Exception {
        final List<RegisteredService> list = new ArrayList<>(5);
        IntStream.range(1, 5).forEach(i -> {
            final RegexRegisteredService r = new RegexRegisteredService();
            r.setServiceId(HTTPS_SERVICE_ID);
            r.setName("testServiceType");
            r.setTheme("testtheme");
            r.setEvaluationOrder(1000);
            r.setId(i * 100);
            list.add(this.dao.save(r));
        });

        list.forEach(r2 -> {
            try {
                Thread.sleep(500);
                this.dao.delete(r2);
                Thread.sleep(2000);
            } catch (final InterruptedException e) {
                throw Throwables.propagate(e);
            }
            assertNull(this.dao.findServiceById(r2.getId()));
        });
    }

    @Test
    public void checkForAuthorizationStrategy() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("checkForAuthorizationStrategy");
        r.setId(42);

        final DefaultRegisteredServiceAccessStrategy authz =
                new DefaultRegisteredServiceAccessStrategy(false, false);

        final Map<String, Set<String>> attrs = new HashMap<>();
        attrs.put("cn", Collections.singleton("v1, v2, v3"));
        attrs.put("memberOf", Collections.singleton("v4, v5, v6"));
        authz.setRequiredAttributes(attrs);
        r.setAccessStrategy(authz);

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());
        assertEquals(r2, r3);
    }

    @Test
    public void verifyAccessStrategyWithStarEndDate() throws Exception {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("verifyAAccessStrategyWithStarEndDate");
        r.setId(62);

        final TimeBasedRegisteredServiceAccessStrategy authz =
                new TimeBasedRegisteredServiceAccessStrategy(true, false);

        authz.setStartingDateTime(ZonedDateTime.now(ZoneOffset.UTC).plusDays(1).toString());
        authz.setEndingDateTime(ZonedDateTime.now(ZoneOffset.UTC).plusDays(10).toString());

        authz.setUnauthorizedRedirectUrl(new URI("https://www.github.com"));
        r.setAccessStrategy(authz);

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());
        assertEquals(r2, r3);
    }

    @Test
    public void verifyAccessStrategyWithEndpoint() throws Exception {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("verifyAccessStrategyWithEndpoint");
        r.setId(62);

        final RemoteEndpointServiceAccessStrategy authz = new RemoteEndpointServiceAccessStrategy();

        authz.setEndpointUrl("http://www.google.com?this=that");
        authz.setAcceptableResponseCodes("200,405,403");
        authz.setUnauthorizedRedirectUrl(new URI("https://www.github.com"));
        r.setAccessStrategy(authz);

        final RegisteredService r2 = this.dao.save(r);
        final RegisteredService r3 = this.dao.findServiceById(r2.getId());
        assertEquals(r2, r3);
    }

    @Test
    public void serializePublicKeyForServiceAndVerify() throws Exception {
        final RegisteredServicePublicKey publicKey = new RegisteredServicePublicKeyImpl(
                "classpath:RSA1024Public.key", "RSA");

        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("serializePublicKeyForServiceAndVerify");
        r.setId(4245);
        r.setPublicKey(publicKey);

        this.dao.save(r);
        this.dao.load();
        assertNotNull(this.dao.findServiceById(r.getId()));
    }

    @Test
    public void checkNullabilityOfAccessStrategy() {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("checkNullabilityOfAccessStrategy");
        r.setId(43210);
        r.setAccessStrategy(null);
        this.dao.save(r);
        this.dao.load();
        final RegisteredService s = this.dao.findServiceById(43210);
        assertNotNull(s);
        assertNotNull(s.getAccessStrategy());
    }

    @Test
    public void persistCustomServiceProperties() throws Exception {
        final RegexRegisteredService r = new RegexRegisteredService();
        r.setServiceId(HTTPS_SERVICE_ID);
        r.setName("persistCustomServiceProperties");
        r.setId(4245);

        final Map<String, RegisteredServiceProperty> properties = new HashMap<>();
        final DefaultRegisteredServiceProperty property = new DefaultRegisteredServiceProperty();
        final Set<String> values = new HashSet<>();
        values.add("value1");
        values.add("value2");
        property.setValues(values);
        properties.put("field1", property);

        final DefaultRegisteredServiceProperty property2 = new DefaultRegisteredServiceProperty();
        final Set<String> values2 = new HashSet<>();
        values2.add("value12");
        values2.add("value22");
        property2.setValues(values2);
        properties.put("field2", property2);

        r.setProperties(properties);

        this.dao.save(r);
        this.dao.load();
        assertNotNull(this.dao.findServiceById(r.getId()));
        assertEquals(r.getProperties().size(), 2);
        assertNotNull(r.getProperties().get("field1"));

        final RegisteredServiceProperty prop = r.getProperties().get("field1");
        assertEquals(prop.getValues().size(), 2);
    }
}
