/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed 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.keycloak.testsuite.admin.authentication;

import org.junit.Assert;
import org.junit.Test;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;

import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
 */
public class InitialFlowsTest extends AbstractAuthenticationTest {

    private HashMap<String, AuthenticatorConfigRepresentation> configs = new HashMap<>();
    private HashMap<String, AuthenticatorConfigRepresentation> expectedConfigs = new HashMap<>();

    {
        expectedConfigs.put("idp-review-profile", newConfig("review profile config", new String[]{"update.profile.on.first.login", "missing"}));
        expectedConfigs.put("idp-create-user-if-unique", newConfig("create unique user config", new String[]{"require.password.update.after.registration", "false"}));
    }

    @Test
    public void testInitialFlows() {

        List<FlowExecutions> result = new LinkedList<>();

        // get all flows
        List<AuthenticationFlowRepresentation> flows = authMgmtResource.getFlows();
        for (AuthenticationFlowRepresentation flow : flows) {
            // get all executions for flow
            List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions(flow.getAlias());

            for (AuthenticationExecutionInfoRepresentation exec : executionReps) {
                // separately load referenced configurations
                String configId = exec.getAuthenticationConfig();
                if (configId != null && !configs.containsKey(configId)) {
                    configs.put(configId, authMgmtResource.getAuthenticatorConfig(configId));
                }
            }
            result.add(new FlowExecutions(flow, executionReps));
        }

        // make sure received flows and their details are as expected
        compare(expectedFlows(), orderAlphabetically(result));
    }

    private void compare(List<FlowExecutions> expected, List<FlowExecutions> actual) {
        Assert.assertEquals("Flow count", expected.size(), actual.size());
        Iterator<FlowExecutions> it1 = expected.iterator();
        Iterator<FlowExecutions> it2 = actual.iterator();
        while (it1.hasNext()) {
            FlowExecutions fe1 = it1.next();
            FlowExecutions fe2 = it2.next();

            compareFlows(fe1.flow, fe2.flow);
            compareExecutionsInfo(fe1.executions, fe2.executions);
        }
    }


    private void compareExecutionsInfo(List<AuthenticationExecutionInfoRepresentation> expected, List<AuthenticationExecutionInfoRepresentation> actual) {
        Assert.assertEquals("Executions count", expected.size(), actual.size());
        Iterator<AuthenticationExecutionInfoRepresentation> it1 = expected.iterator();
        Iterator<AuthenticationExecutionInfoRepresentation> it2 = actual.iterator();
        while (it1.hasNext()) {
            AuthenticationExecutionInfoRepresentation exe1 = it1.next();
            AuthenticationExecutionInfoRepresentation exe2 = it2.next();

            compareExecutionWithConfig(exe1, exe2);
        }
    }

    private void compareExecutionWithConfig(AuthenticationExecutionInfoRepresentation expected, AuthenticationExecutionInfoRepresentation actual) {
        super.compareExecution(expected, actual);
        compareAuthConfig(expected, actual);
    }

    private void compareAuthConfig(AuthenticationExecutionInfoRepresentation expected, AuthenticationExecutionInfoRepresentation actual) {
        AuthenticatorConfigRepresentation cfg1 = expectedConfigs.get(expected.getProviderId());
        AuthenticatorConfigRepresentation cfg2 = configs.get(actual.getAuthenticationConfig());

        if (cfg1 == null && cfg2 == null) {
            return;
        }
        Assert.assertEquals("Execution configuration alias", cfg1.getAlias(), cfg2.getAlias());
        Assert.assertEquals("Execution configuration params", cfg1.getConfig(), cfg2.getConfig());
    }

    private List<FlowExecutions> orderAlphabetically(List<FlowExecutions> result) {
        List<FlowExecutions> sorted = new ArrayList<>(result);
        Collections.sort(sorted);
        return sorted;
    }

    private LinkedList<FlowExecutions> expectedFlows() {
        LinkedList<FlowExecutions> expected = new LinkedList<>();

        AuthenticationFlowRepresentation flow = newFlow("browser", "browser based authentication", "basic-flow", true, true);
        addExecExport(flow, null, false, "auth-cookie", false, null, ALTERNATIVE, 10);
        addExecExport(flow, null, false, "auth-spnego", false, null, DISABLED, 20);
        addExecExport(flow, "forms", false, null, true, null, ALTERNATIVE, 30);

        List<AuthenticationExecutionInfoRepresentation> execs = new LinkedList<>();
        addExecInfo(execs, "Cookie", "auth-cookie", false, 0, 0, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED});
        addExecInfo(execs, "Kerberos", "auth-spnego", false, 0, 1, DISABLED, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
        addExecInfo(execs, "forms", null, false, 0, 2, ALTERNATIVE, true, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
        addExecInfo(execs, "Username Password Form", "auth-username-password-form", false, 1, 0, REQUIRED, null, new String[]{REQUIRED});
        addExecInfo(execs, "OTP Form", "auth-otp-form", false, 1, 1, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
        expected.add(new FlowExecutions(flow, execs));

        flow = newFlow("clients", "Base authentication for clients", "client-flow", true, true);
        addExecExport(flow, null, false, "client-secret", false, null, ALTERNATIVE, 10);
        addExecExport(flow, null, false, "client-jwt", false, null, ALTERNATIVE, 20);

        execs = new LinkedList<>();
        addExecInfo(execs, "Client Id and Secret", "client-secret", false, 0, 0, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED});
        addExecInfo(execs, "Signed Jwt", "client-jwt", false, 0, 1, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED});
        expected.add(new FlowExecutions(flow, execs));

        flow = newFlow("direct grant", "OpenID Connect Resource Owner Grant", "basic-flow", true, true);
        addExecExport(flow, null, false, "direct-grant-validate-username", false, null, REQUIRED, 10);
        addExecExport(flow, null, false, "direct-grant-validate-password", false, null, REQUIRED, 20);
        addExecExport(flow, null, false, "direct-grant-validate-otp", false, null, OPTIONAL, 30);

        execs = new LinkedList<>();
        addExecInfo(execs, "Username Validation", "direct-grant-validate-username", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
        addExecInfo(execs, "Password", "direct-grant-validate-password", false, 0, 1, REQUIRED, null, new String[]{REQUIRED, DISABLED});
        addExecInfo(execs, "OTP", "direct-grant-validate-otp", false, 0, 2, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
        expected.add(new FlowExecutions(flow, execs));

        flow = newFlow("first broker login", "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
                "basic-flow", true, true);
        addExecExport(flow, null, false, "idp-review-profile", false, "review profile config", REQUIRED, 10);
        addExecExport(flow, null, false, "idp-create-user-if-unique", false, "create unique user config", ALTERNATIVE, 20);
        addExecExport(flow, "Handle Existing Account", false, null, true, null, ALTERNATIVE, 30);

        execs = new LinkedList<>();
        addExecInfo(execs, "Review Profile", "idp-review-profile", true, 0, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED});
        addExecInfo(execs, "Create User If Unique", "idp-create-user-if-unique", true, 0, 1, ALTERNATIVE, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
        addExecInfo(execs, "Handle Existing Account", null, false, 0, 2, ALTERNATIVE, true, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
        addExecInfo(execs, "Confirm link existing account", "idp-confirm-link", false, 1, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED});
        addExecInfo(execs, "Verify existing account by Email", "idp-email-verification", false, 1, 1, ALTERNATIVE, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
        addExecInfo(execs, "Verify Existing Account by Re-authentication", null, false, 1, 2, ALTERNATIVE, true, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
        addExecInfo(execs, "Username Password Form for identity provider reauthentication", "idp-username-password-form", false, 2, 0, REQUIRED, null, new String[]{REQUIRED});
        addExecInfo(execs, "OTP Form", "auth-otp-form", false, 2, 1, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
        expected.add(new FlowExecutions(flow, execs));

        flow = newFlow("registration", "registration flow", "basic-flow", true, true);
        addExecExport(flow, "registration form", false, "registration-page-form", true, null, REQUIRED, 10);

        execs = new LinkedList<>();
        addExecInfo(execs, "registration form", "registration-page-form", false, 0, 0, REQUIRED, true, new String[]{REQUIRED, DISABLED});
        addExecInfo(execs, "Registration User Creation", "registration-user-creation", false, 1, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED});
        addExecInfo(execs, "Profile Validation", "registration-profile-action", false, 1, 1, REQUIRED, null, new String[]{REQUIRED, DISABLED});
        addExecInfo(execs, "Password Validation", "registration-password-action", false, 1, 2, REQUIRED, null, new String[]{REQUIRED, DISABLED});
        addExecInfo(execs, "Recaptcha", "registration-recaptcha-action", true, 1, 3, DISABLED, null, new String[]{REQUIRED, DISABLED});
        expected.add(new FlowExecutions(flow, execs));

        flow = newFlow("reset credentials", "Reset credentials for a user if they forgot their password or something", "basic-flow", true, true);
        addExecExport(flow, null, false, "reset-credentials-choose-user", false, null, REQUIRED, 10);
        addExecExport(flow, null, false, "reset-credential-email", false, null, REQUIRED, 20);
        addExecExport(flow, null, false, "reset-password", false, null, REQUIRED, 30);
        addExecExport(flow, null, false, "reset-otp", false, null, OPTIONAL, 40);

        execs = new LinkedList<>();
        addExecInfo(execs, "Choose User", "reset-credentials-choose-user", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
        addExecInfo(execs, "Send Reset Email", "reset-credential-email", false, 0, 1, REQUIRED, null, new String[]{REQUIRED});
        addExecInfo(execs, "Reset Password", "reset-password", false, 0, 2, REQUIRED, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
        addExecInfo(execs, "Reset OTP", "reset-otp", false, 0, 3, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
        expected.add(new FlowExecutions(flow, execs));

        flow = newFlow("saml ecp", "SAML ECP Profile Authentication Flow", "basic-flow", true, true);
        addExecExport(flow, null, false, "http-basic-authenticator", false, null, REQUIRED, 10);

        execs = new LinkedList<>();
        addExecInfo(execs, "HTTP Basic Authentication", "http-basic-authenticator", false, 0, 0, REQUIRED, null, new String[]{});
        expected.add(new FlowExecutions(flow, execs));

        return expected;
    }

    private void addExecExport(AuthenticationFlowRepresentation flow, String flowAlias, boolean userSetupAllowed,
                               String authenticator, boolean authenticatorFlow, String authenticatorConfig,
                               String requirement, int priority) {

        AuthenticationExecutionExportRepresentation rep = newExecutionExportRepresentation(flowAlias, userSetupAllowed,
                authenticator, authenticatorFlow, authenticatorConfig, requirement, priority);

        List<AuthenticationExecutionExportRepresentation> execs = flow.getAuthenticationExecutions();
        if (execs == null) {
            execs = new ArrayList<>();
            flow.setAuthenticationExecutions(execs);
        }
        execs.add(rep);
    }

    private AuthenticationExecutionExportRepresentation newExecutionExportRepresentation(String flowAlias, boolean userSetupAllowed, String authenticator, boolean authenticatorFlow, String authenticatorConfig, String requirement, int priority) {
        AuthenticationExecutionExportRepresentation rep = new AuthenticationExecutionExportRepresentation();
        rep.setFlowAlias(flowAlias);
        rep.setUserSetupAllowed(userSetupAllowed);
        rep.setAuthenticator(authenticator);
        rep.setAutheticatorFlow(authenticatorFlow);
        rep.setAuthenticatorConfig(authenticatorConfig);
        rep.setRequirement(requirement);
        rep.setPriority(priority);
        return rep;
    }

    private static class FlowExecutions implements Comparable<FlowExecutions> {
        AuthenticationFlowRepresentation flow;
        List<AuthenticationExecutionInfoRepresentation> executions;

        FlowExecutions(AuthenticationFlowRepresentation flow, List<AuthenticationExecutionInfoRepresentation> executions) {
            this.flow = flow;
            this.executions = executions;
        }

        @Override
        public int compareTo(FlowExecutions o) {
            return flow.getAlias().compareTo(o.flow.getAlias());
        }
    }
}
