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

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.openqa.selenium.NoSuchElementException;

import java.util.List;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityProviderTest {

    private static final int PORT = 8082;

    private static Keycloak keycloak1;
    private static Keycloak keycloak2;

    @ClassRule
    public static AbstractKeycloakRule oidcServerRule = new AbstractKeycloakRule() {

        @Override
        protected void configureServer(KeycloakServer server) {
            server.getConfig().setPort(PORT);
        }

        @Override
        protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
            server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json"));

            // Disable update profile
            RealmModel realm = getRealm(session);
            setUpdateProfileFirstLogin(realm, IdentityProviderRepresentation.UPFLM_OFF);
        }

        @Override
        protected String[] getTestRealms() {
            return new String[] { "realm-with-oidc-identity-provider" };
        }
    };


    @BeforeClass
    public static void before() {
        keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
        keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);

        // Require broker to show consent screen
        RealmResource brokeredRealm = keycloak2.realm("realm-with-oidc-identity-provider");
        List<ClientRepresentation> clients = brokeredRealm.clients().findByClientId("broker-app");
        Assert.assertEquals(1, clients.size());
        ClientRepresentation brokerApp = clients.get(0);
        brokerApp.setConsentRequired(true);
        brokeredRealm.clients().get(brokerApp.getId()).update(brokerApp);


        // Change timeouts on realm-with-broker to lower values
        RealmResource realmWithBroker = keycloak1.realm("realm-with-broker");
        RealmRepresentation realmRep = realmWithBroker.toRepresentation();
        realmRep.setAccessCodeLifespanLogin(30);;
        realmRep.setAccessCodeLifespan(30);
        realmRep.setAccessCodeLifespanUserAction(30);
        realmWithBroker.update(realmRep);
    }


    @Override
    protected String getProviderId() {
        return "kc-oidc-idp";
    }


    // KEYCLOAK-2769
    @Test
    public void testConsentDeniedWithExpiredClientSession() throws Exception {
        // Login to broker
        loginIDP("test-user");

        // Set time offset
        Time.setOffset(60);
        try {
            // User rejected consent
            grantPage.assertCurrent();
            grantPage.cancel();

            // Assert error page with backToApplication link displayed
            errorPage.assertCurrent();
            errorPage.clickBackToApplication();

            assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));

        } finally {
            Time.setOffset(0);
        }
    }


    // KEYCLOAK-2769
    @Test
    public void testConsentDeniedWithExpiredAndClearedClientSession() throws Exception {
        // Login to broker again
        loginIDP("test-user");

        // Set time offset
        Time.setOffset(60);
        try {
            // Manually remove expiredSessions TODO: Will require custom endpoint when migrate to integration-arquillian
            brokerServerRule.stopSession(this.session, true);
            this.session = brokerServerRule.startSession();

            session.sessions().removeExpired(getRealm());

            brokerServerRule.stopSession(this.session, true);
            this.session = brokerServerRule.startSession();

            // User rejected consent
            grantPage.assertCurrent();
            grantPage.cancel();

            // Assert error page without backToApplication link (clientSession expired and was removed on the server)
            errorPage.assertCurrent();
            try {
                errorPage.clickBackToApplication();
                fail("Not expected to have link backToApplication available");
            } catch (NoSuchElementException nsee) {
                // Expected;
            }

        } finally {
            Time.setOffset(0);
        }
    }


    // KEYCLOAK-2801
    @Test
    public void testAccountManagementLinkingAndExpiredClientSession() throws Exception {
        // Login as pedroigor to account management
        loginToAccountManagement("pedroigor");

        // Link my "pedroigor" identity with "test-user" from brokered Keycloak
        accountFederatedIdentityPage.clickAddProvider(getProviderId());

        assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
        this.loginPage.login("test-user", "password");

        // Set time offset
        Time.setOffset(60);
        try {
            // User rejected consent
            grantPage.assertCurrent();
            grantPage.cancel();

            // Assert account error page with "staleCodeAccount" error displayed
            accountFederatedIdentityPage.assertCurrent();
            Assert.assertEquals("The page expired. Please try one more time.", accountFederatedIdentityPage.getError());


            // Try to link one more time
            accountFederatedIdentityPage.clickAddProvider(getProviderId());

            assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
            this.loginPage.login("test-user", "password");

            Time.setOffset(120);

            // User granted consent
            grantPage.assertCurrent();
            grantPage.accept();

            // Assert account error page with "staleCodeAccount" error displayed
            accountFederatedIdentityPage.assertCurrent();
            Assert.assertEquals("The page expired. Please try one more time.", accountFederatedIdentityPage.getError());

        } finally {
            Time.setOffset(0);
        }

        // Revoke consent
        RealmResource brokeredRealm = keycloak2.realm("realm-with-oidc-identity-provider");
        List<UserRepresentation> users = brokeredRealm.users().search("test-user", 0, 1);
        brokeredRealm.users().get(users.get(0).getId()).revokeConsent("broker-app");
    }


    @Test
    public void testLoginCancelConsent() throws Exception {
        // Try to login
        loginIDP("test-user");

        // User rejected consent
        grantPage.assertCurrent();
        grantPage.cancel();

        // Assert back on login page
        assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/"));
        assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
    }


    // KEYCLOAK-2802
    @Test
    public void testAccountManagementLinkingCancelConsent() throws Exception {
        // Login as pedroigor to account management
        loginToAccountManagement("pedroigor");

        // Link my "pedroigor" identity with "test-user" from brokered Keycloak
        accountFederatedIdentityPage.clickAddProvider(getProviderId());

        assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
        this.loginPage.login("test-user", "password");

        // User rejected consent
        grantPage.assertCurrent();
        grantPage.cancel();

        // Assert account error page with "consentDenied" error displayed
        accountFederatedIdentityPage.assertCurrent();
        Assert.assertEquals("Consent denied.", accountFederatedIdentityPage.getError());
    }


    private void loginToAccountManagement(String username) {
        accountFederatedIdentityPage.realm("realm-with-broker");
        accountFederatedIdentityPage.open();
        assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
        loginPage.login(username, "password");
        assertTrue(accountFederatedIdentityPage.isCurrent());
    }
}
