/*
 * 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.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import javax.ws.rs.core.Response;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.Predicate;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.common.lib.to.TaskTO;
import org.apache.syncope.common.lib.to.AnyTypeClassTO;
import org.apache.syncope.common.lib.to.AttrTO;
import org.apache.syncope.common.lib.to.BulkAction;
import org.apache.syncope.common.lib.to.ConnInstanceTO;
import org.apache.syncope.common.lib.to.GroupTO;
import org.apache.syncope.common.lib.to.ItemTO;
import org.apache.syncope.common.lib.to.MappingTO;
import org.apache.syncope.common.lib.to.MembershipTO;
import org.apache.syncope.common.lib.to.PlainSchemaTO;
import org.apache.syncope.common.lib.to.ProvisionTO;
import org.apache.syncope.common.lib.to.PullTaskTO;
import org.apache.syncope.common.lib.to.RealmTO;
import org.apache.syncope.common.lib.to.ResourceTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.AttrSchemaType;
import org.apache.syncope.common.lib.types.ConnConfPropSchema;
import org.apache.syncope.common.lib.types.ConnConfProperty;
import org.apache.syncope.common.lib.types.ConnectorCapability;
import org.apache.syncope.common.lib.types.MappingPurpose;
import org.apache.syncope.common.lib.types.PullMode;
import org.apache.syncope.common.lib.types.SchemaType;
import org.apache.syncope.common.lib.types.TaskType;
import org.apache.syncope.common.rest.api.beans.AnyQuery;
import org.apache.syncope.common.rest.api.beans.TaskQuery;
import org.apache.syncope.common.rest.api.service.ConnectorService;
import org.apache.syncope.common.rest.api.service.TaskService;
import org.apache.syncope.core.migration.MigrationPullActions;
import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:migrationEnv.xml" })
public class MigrationITCase extends AbstractTaskITCase {

    private static final String CONNINSTANCE_DISPLAY_NAME = "syncope12DB";

    private static final String RESOURCE_KEY = "Syncope 1.2";

    private static final String MIGRATION_CIPHER_ALGORITHM = "migrationCipherAlgorithm";

    private static final String MIGRATION_RESOURCES_SCHEMA = "migrationResources";

    private static final String MIGRATION_MEMBERSHIPS_SCHEMA = "migrationMemberships";

    private static final String MIGRATION_ANYTYPE_CLASS = "migration";

    private static final String MIGRATION_REALM = "migration";

    private static final String PULL_TASK_NAME = "Syncope 1.2 migration";

    private static String basedir;

    private static String connectorServerLocation;

    private static String connIdDbVersion;

    @BeforeClass
    public static void setup() throws IOException {
        try {
            Properties props = new Properties();
            InputStream propStream = MigrationITCase.class.getResourceAsStream("/test.properties");
            props.load(propStream);
            propStream.close();

            propStream = MigrationITCase.class.getResourceAsStream("/connid.properties");
            props.load(propStream);
            propStream.close();

            basedir = props.getProperty("basedir");

            for (String location : props.getProperty("connid.locations").split(",")) {
                if (!location.startsWith("file")) {
                    connectorServerLocation = location;
                }
            }

            connIdDbVersion = props.getProperty("connid.database.version");
        } catch (Exception e) {
            LOG.error("Could not load /connid.properties", e);
        }

        assertNotNull(basedir);
        assertNotNull(connectorServerLocation);
        assertNotNull(connIdDbVersion);
    }

    @Autowired
    private DriverManagerDataSource syncope12DataSource;

    private String setupAnyTypeClass() {
        PlainSchemaTO cipherAlgorithm = new PlainSchemaTO();
        cipherAlgorithm.setKey(MIGRATION_CIPHER_ALGORITHM);
        cipherAlgorithm.setType(AttrSchemaType.String);
        cipherAlgorithm.setReadonly(true);
        cipherAlgorithm = createSchema(SchemaType.PLAIN, cipherAlgorithm);

        PlainSchemaTO migrationResources = new PlainSchemaTO();
        migrationResources.setKey(MIGRATION_RESOURCES_SCHEMA);
        migrationResources.setType(AttrSchemaType.String);
        migrationResources.setMultivalue(true);
        migrationResources.setReadonly(true);
        migrationResources = createSchema(SchemaType.PLAIN, migrationResources);

        PlainSchemaTO migrationMemberships = new PlainSchemaTO();
        migrationMemberships.setKey(MIGRATION_MEMBERSHIPS_SCHEMA);
        migrationMemberships.setType(AttrSchemaType.String);
        migrationMemberships.setMultivalue(true);
        migrationMemberships.setReadonly(true);
        migrationMemberships = createSchema(SchemaType.PLAIN, migrationMemberships);

        AnyTypeClassTO migration = new AnyTypeClassTO();
        migration.setKey(MIGRATION_ANYTYPE_CLASS);
        migration.getPlainSchemas().add(cipherAlgorithm.getKey());
        migration.getPlainSchemas().add(migrationResources.getKey());
        migration.getPlainSchemas().add(migrationMemberships.getKey());

        Response response = anyTypeClassService.create(migration);
        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatusInfo().getStatusCode());

        return MIGRATION_ANYTYPE_CLASS;
    }

    private String setupConnector() {
        ConnInstanceTO connInstanceTO = new ConnInstanceTO();
        connInstanceTO.setAdminRealm(SyncopeConstants.ROOT_REALM);
        connInstanceTO.setLocation(connectorServerLocation);
        connInstanceTO.setConnectorName("net.tirasa.connid.bundles.db.scriptedsql.ScriptedSQLConnector");
        connInstanceTO.setBundleName("net.tirasa.connid.bundles.db.scriptedsql");
        connInstanceTO.setVersion(connIdDbVersion);
        connInstanceTO.setDisplayName(CONNINSTANCE_DISPLAY_NAME);

        ConnConfPropSchema schema = new ConnConfPropSchema();
        schema.setName("user");
        schema.setType(String.class.getName());
        ConnConfProperty property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add(syncope12DataSource.getUsername());
        connInstanceTO.getConf().add(property);

        schema = new ConnConfPropSchema();
        schema.setName("password");
        schema.setType(GuardedString.class.getName());
        property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add(syncope12DataSource.getPassword());
        connInstanceTO.getConf().add(property);

        schema = new ConnConfPropSchema();
        schema.setName("jdbcDriver");
        property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add("org.h2.Driver");
        connInstanceTO.getConf().add(property);

        schema = new ConnConfPropSchema();
        schema.setName("jdbcUrlTemplate");
        property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add(syncope12DataSource.getUrl());
        connInstanceTO.getConf().add(property);

        schema = new ConnConfPropSchema();
        schema.setName("testScriptFileName");
        property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add(basedir + "/../../core/migration/src/main/resources/scripted/TestScript.groovy");
        connInstanceTO.getConf().add(property);

        schema = new ConnConfPropSchema();
        schema.setName("schemaScriptFileName");
        property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add(basedir + "/../../core/migration/src/main/resources/scripted/SchemaScript.groovy");
        connInstanceTO.getConf().add(property);

        schema = new ConnConfPropSchema();
        schema.setName("searchScriptFileName");
        property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add(basedir + "/../../core/migration/src/main/resources/scripted/SearchScript.groovy");
        connInstanceTO.getConf().add(property);

        schema = new ConnConfPropSchema();
        schema.setName("syncScriptFileName");
        property = new ConnConfProperty();
        property.setSchema(schema);
        property.getValues().add(basedir + "/../../core/migration/src/main/resources/scripted/SyncScript.groovy");
        connInstanceTO.getConf().add(property);

        connInstanceTO.getCapabilities().add(ConnectorCapability.SEARCH);
        connInstanceTO.getCapabilities().add(ConnectorCapability.SYNC);

        Response response = connectorService.create(connInstanceTO);
        connInstanceTO = getObject(response.getLocation(), ConnectorService.class, ConnInstanceTO.class);

        try {
            connectorService.check(connInstanceTO);
        } catch (Exception e) {
            fail("Unexpected exception:\n" + ExceptionUtils2.getFullStackTrace(e));
        }

        return connInstanceTO.getKey();
    }

    private void setupResource(final String connectorKey, final String anyTypeClass) {
        ResourceTO resourceTO = new ResourceTO();

        resourceTO.setKey(RESOURCE_KEY);
        resourceTO.setConnector(connectorKey);

        // USER
        ProvisionTO provisionTO = new ProvisionTO();
        provisionTO.setAnyType(AnyTypeKind.USER.name());
        provisionTO.setObjectClass(ObjectClass.ACCOUNT_NAME);
        provisionTO.getAuxClasses().add(anyTypeClass);
        resourceTO.getProvisions().add(provisionTO);

        MappingTO mapping = new MappingTO();
        provisionTO.setMapping(mapping);

        ItemTO item = new ItemTO();
        item.setIntAttrName("username");
        item.setExtAttrName("username");
        item.setMandatoryCondition("true");
        item.setPurpose(MappingPurpose.PULL);
        mapping.setConnObjectKeyItem(item);

        item = new ItemTO();
        item.setPassword(true);
        item.setIntAttrName("password");
        item.setExtAttrName("__PASSWORD__");
        item.setMandatoryCondition("true");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName(MIGRATION_CIPHER_ALGORITHM);
        item.setExtAttrName("cipherAlgorithm");
        item.setMandatoryCondition("true");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("surname");
        item.setExtAttrName("surname");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("email");
        item.setExtAttrName("email");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("firstname");
        item.setExtAttrName("firstname");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("ctype");
        item.setExtAttrName("type");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("gender");
        item.setExtAttrName("gender");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("loginDate");
        item.setExtAttrName("loginDate");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName(MIGRATION_RESOURCES_SCHEMA);
        item.setExtAttrName("__RESOURCES__");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        // GROUP
        provisionTO = new ProvisionTO();
        provisionTO.setAnyType(AnyTypeKind.GROUP.name());
        provisionTO.setObjectClass(ObjectClass.GROUP_NAME);
        provisionTO.getAuxClasses().add(anyTypeClass);
        resourceTO.getProvisions().add(provisionTO);

        mapping = new MappingTO();
        provisionTO.setMapping(mapping);

        item = new ItemTO();
        item.setIntAttrName("name");
        item.setExtAttrName("name");
        item.setMandatoryCondition("true");
        item.setPurpose(MappingPurpose.PULL);
        mapping.setConnObjectKeyItem(item);

        item = new ItemTO();
        item.setIntAttrName("show");
        item.setExtAttrName("show");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("title");
        item.setExtAttrName("title");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName("icon");
        item.setExtAttrName("icon");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName(MIGRATION_RESOURCES_SCHEMA);
        item.setExtAttrName("__RESOURCES__");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        item = new ItemTO();
        item.setIntAttrName(MIGRATION_MEMBERSHIPS_SCHEMA);
        item.setExtAttrName("__MEMBERSHIPS__");
        item.setMandatoryCondition("false");
        item.setPurpose(MappingPurpose.PULL);
        mapping.add(item);

        resourceService.create(resourceTO);
    }

    private void setupRealm() {
        try {
            realmService.list("/" + MIGRATION_REALM);
        } catch (SyncopeClientException e) {
            LOG.error("{} not found? Let's attempt to re-create...", MIGRATION_REALM, e);

            RealmTO realm = new RealmTO();
            realm.setName(MIGRATION_REALM);
            realmService.create("/", realm);
        }
    }

    private String setupPullTask() {
        PullTaskTO task = new PullTaskTO();
        task.setActive(true);
        task.setName(PULL_TASK_NAME);
        task.setResource(RESOURCE_KEY);
        task.setPerformCreate(true);
        task.setSyncStatus(true);
        task.setPullMode(PullMode.FULL_RECONCILIATION);
        task.setDestinationRealm("/" + MIGRATION_REALM);
        task.getActionsClassNames().add(MigrationPullActions.class.getName());

        UserTO user = new UserTO();
        user.getPlainAttrs().add(new AttrTO.Builder().schema("userId").value("'12' + username + '@syncope.apache.org'").
                build());
        user.getPlainAttrs().add(new AttrTO.Builder().schema("fullname").value("username").build());
        task.getTemplates().put(AnyTypeKind.USER.name(), user);

        Response response = taskService.create(TaskType.PULL, task);
        task = getObject(response.getLocation(), TaskService.class, PullTaskTO.class);

        return task.getKey();
    }

    @Test
    public void migrateFromSyncope12() throws InterruptedException {
        // 1. cleanup
        try {
            for (TaskTO task : taskService.search(
                    new TaskQuery.Builder(TaskType.PULL).resource(RESOURCE_KEY).build()).getResult()) {

                if (PULL_TASK_NAME.equals(PullTaskTO.class.cast(task).getName())) {
                    taskService.delete(TaskType.PULL, task.getKey());
                }
            }
        } catch (Exception e) {
            // ignore
        }
        try {
            resourceService.delete(RESOURCE_KEY);
        } catch (Exception e) {
            // ignore
        }
        try {
            for (ConnInstanceTO connInstance : connectorService.list(null)) {
                if (CONNINSTANCE_DISPLAY_NAME.equals(connInstance.getDisplayName())) {
                    connectorService.delete(connInstance.getKey());
                }
            }
        } catch (Exception e) {
            // ignore
        }
        try {
            schemaService.delete(SchemaType.PLAIN, MIGRATION_CIPHER_ALGORITHM);
            schemaService.delete(SchemaType.PLAIN, MIGRATION_MEMBERSHIPS_SCHEMA);
            schemaService.delete(SchemaType.PLAIN, MIGRATION_RESOURCES_SCHEMA);
            anyTypeClassService.delete(MIGRATION_ANYTYPE_CLASS);
        } catch (Exception e) {
            // ignore
        }

        BulkAction bulkAction = new BulkAction();
        bulkAction.setType(BulkAction.Type.DELETE);

        for (UserTO user : userService.search(new AnyQuery.Builder().fiql("username==*12").build()).getResult()) {
            bulkAction.getTargets().add(user.getKey());
        }
        userService.bulk(bulkAction);

        bulkAction.getTargets().clear();
        for (GroupTO group : groupService.search(new AnyQuery.Builder().fiql("name==*12").build()).getResult()) {
            bulkAction.getTargets().add(group.getKey());
        }
        groupService.bulk(bulkAction);

        // 2. setup
        setupResource(setupConnector(), setupAnyTypeClass());
        setupRealm();
        String pullTaskKey = setupPullTask();

        // 3. execute pull task
        execProvisioningTask(taskService, TaskType.PULL, pullTaskKey, 50, false);

        // 4. verify
        UserTO user = null;

        int i = 0;
        boolean membershipFound = false;
        do {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }

            try {
                user = userService.read("rossini12");
                assertNotNull(user);

                membershipFound = IterableUtils.matchesAny(user.getMemberships(), new Predicate<MembershipTO>() {

                    @Override
                    public boolean evaluate(final MembershipTO object) {
                        return "1 root12".equals(object.getGroupName());
                    }
                });
            } catch (Exception e) {
                // ignore
            }

            i++;
        } while (!membershipFound && i < 50);
        assertNotNull(user);
        assertTrue(membershipFound);

        assertEquals("/" + MIGRATION_REALM, user.getRealm());
        GroupTO group = groupService.read("12 aRoleForPropagation12");
        assertNotNull(group);
        assertEquals("/" + MIGRATION_REALM, group.getRealm());

        // 4a. user plain attrs
        assertEquals("Gioacchino", user.getPlainAttr("firstname").getValues().get(0));
        assertEquals("Rossini", user.getPlainAttr("surname").getValues().get(0));

        // 4b. user resources
        assertTrue(user.getResources().contains(RESOURCE_NAME_TESTDB2));

        // 4c. user password
        assertNotNull(clientFactory.create("bellini12", ADMIN_PWD).self());

        // 4d. group plain attrs
        assertEquals("r12", group.getPlainAttr("title").getValues().get(0));

        // 4e. group resources
        assertTrue(group.getResources().contains(RESOURCE_NAME_CSV));
    }
}
