/*
 * 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.forms;

import com.google.common.collect.ImmutableMap;
import org.apache.commons.io.IOUtils;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.common.Profile;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.ExecutionBuilder;
import org.keycloak.testsuite.util.FlowBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;

import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Map;

/**
 * Tests for {@link org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticator}
 *
 * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
 */
@EnableFeature(Profile.Feature.UPLOAD_SCRIPTS)
public class ScriptAuthenticatorTest extends AbstractFlowTest {

    @Page
    protected LoginPage loginPage;

    @Rule
    public AssertEvents events = new AssertEvents(this);

    private AuthenticationFlowRepresentation flow;

    public static final String EXECUTION_ID = "scriptAuth";

    @Override
    public void configureTestRealm(RealmRepresentation testRealm) {

        UserRepresentation failUser = UserBuilder.create()
                .id("fail")
                .username("fail")
                .email("fail@test.com")
                .enabled(true)
                .password("password")
                .build();

        UserRepresentation okayUser = UserBuilder.create()
                .id("user")
                .username("user")
                .email("user@test.com")
                .enabled(true)
                .password("password")
                .build();

        RealmBuilder.edit(testRealm)
                .user(failUser)
                .user(okayUser);
    }

    @Before
    public void configureFlows() throws Exception {
        if (testContext.isInitialized()) {
            return;
        }

        String scriptFlow = "scriptBrowser";

        AuthenticationFlowRepresentation scriptBrowserFlow = FlowBuilder.create()
                .alias(scriptFlow)
                .description("dummy pass through registration")
                .providerId("basic-flow")
                .topLevel(true)
                .builtIn(false)
                .build();

        Response createFlowResponse = testRealm().flows().createFlow(scriptBrowserFlow);
        Assert.assertEquals(201, createFlowResponse.getStatus());

        RealmRepresentation realm = testRealm().toRepresentation();
        realm.setBrowserFlow(scriptFlow);
        realm.setDirectGrantFlow(scriptFlow);
        testRealm().update(realm);

        this.flow = findFlowByAlias(scriptFlow);

        AuthenticationExecutionRepresentation usernamePasswordFormExecution = ExecutionBuilder.create()
                .id("username password form")
                .parentFlow(this.flow.getId())
                .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name())
                .authenticator(UsernamePasswordFormFactory.PROVIDER_ID)
                .build();

        AuthenticationExecutionRepresentation authScriptExecution = ExecutionBuilder.create()
                .id(EXECUTION_ID)
                .parentFlow(this.flow.getId())
                .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name())
                .authenticator(ScriptBasedAuthenticatorFactory.PROVIDER_ID)
                .build();

        Response addExecutionResponse = testRealm().flows().addExecution(usernamePasswordFormExecution);
        Assert.assertEquals(201, addExecutionResponse.getStatus());
        addExecutionResponse.close();

        addExecutionResponse = testRealm().flows().addExecution(authScriptExecution);
        Assert.assertEquals(201, addExecutionResponse.getStatus());
        addExecutionResponse.close();

        testContext.setInitialized(true);
    }

    /**
     * KEYCLOAK-3491
     */
    @Test
    public void loginShouldWorkWithScriptAuthenticator() {
        addConfigFromFile("/scripts/authenticator-example.js");

        loginPage.open();

        loginPage.login("user", "password");

        events.expectLogin().user("user").detail(Details.USERNAME, "user").assertEvent();
    }

    /**
     * KEYCLOAK-3491
     */
    @Test
    public void loginShouldFailWithScriptAuthenticator() {
        addConfigFromFile("/scripts/authenticator-example.js");

        loginPage.open();

        loginPage.login("fail", "password");

        events.expect(EventType.LOGIN_ERROR).user((String) null).error(Errors.USER_NOT_FOUND).assertEvent();
    }

    /**
     * KEYCLOAK-4505
     */
    @Test
    public void scriptWithClientSession()  {
        addConfigFromFile("/scripts/client-session-test.js", ImmutableMap.of(
                "realm", "test",
                "clientId", "test-app",
                "authMethod", "openid-connect"));

        loginPage.open();

        loginPage.login("user", "password");

        events.expectLogin().user("user").detail(Details.USERNAME, "user").assertEvent();
    }

    private void addConfigFromFile(String filename) {
        addConfigFromFile(filename, null);
    }

    private void addConfigFromFile(String filename, Map<String, String> parameters) {

        String alias = filename.substring(filename.lastIndexOf("/") + 1);
        String script = loadFile(filename, parameters);

        Response newExecutionConfigResponse = testRealm().flows().
                newExecutionConfig(EXECUTION_ID, createScriptAuthConfig(EXECUTION_ID, alias, script, "script based authenticator"));
        newExecutionConfigResponse.close();

        Assert.assertEquals(201, newExecutionConfigResponse.getStatus());
    }

    private String loadFile(String filename, Map<String, String> parameters) {
        String script = null;
        try {
            script = IOUtils.toString(getClass().getResourceAsStream(filename));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        if (parameters != null) {
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                script = script.replaceAll("\\$\\{" + entry.getKey() + "}", entry.getValue());
            }
        }

        return script;
    }

    private AuthenticatorConfigRepresentation createScriptAuthConfig(String alias, String scriptName, String script, String scriptDescription) {

        AuthenticatorConfigRepresentation configRep = new AuthenticatorConfigRepresentation();
        configRep.setAlias(alias);
        configRep.getConfig().put("scriptCode", script);
        configRep.getConfig().put("scriptName", scriptName);
        configRep.getConfig().put("scriptDescription", scriptDescription);

        return configRep;
    }
}

