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

import org.apache.commons.collections.map.HashedMap;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.junit.Before;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.common.KeycloakEvaluationContext;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.common.ClientConnection;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.util.JsonSerialization;

import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
 */
public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest {

    protected ResourceServer resourceServer;
    protected Resource adminResource;
    protected Policy anyAdminPolicy;
    protected Policy onlyFromSpecificAddressPolicy;
    protected Policy administrationPolicy;

    protected Resource albumResource;
    protected Policy anyUserPolicy;

    @Before
    public void onBefore() {
        super.onBefore();
        this.resourceServer = createResourceServer();
        this.adminResource = createAdminAlbumResource();
        this.anyAdminPolicy = createAnyAdminPolicy();
        this.onlyFromSpecificAddressPolicy = createOnlyFromSpecificAddressPolicy();
        this.administrationPolicy = createAdministrationPolicy();

        this.albumResource = createAlbumResource();
        this.anyUserPolicy = createAnyUserPolicy();
    }

    protected ResourceServer createResourceServer() {
        return onAuthorizationSession(authorizationProvider -> {
            StoreFactory storeFactory = authorizationProvider.getStoreFactory();
            ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();

            ResourceServer resourceServer = resourceServerStore.create(getClientByClientId("photoz-restful-api").getId());

            resourceServer.setAllowRemoteResourceManagement(true);

            return resourceServer;
        });
    }

    protected Map<String, DefaultEvaluation> performEvaluation(List<ResourcePermission> permissions, AccessToken accessToken, ClientConnection clientConnection) {
        Map<String, DefaultEvaluation> evaluations = new HashedMap();

        onAuthorizationSession(authorizationProvider -> {
            StoreFactory storeFactory = authorizationProvider.getStoreFactory();

            // during tests we create resource instances, but we need to reload them to get their collections updated
            List<ResourcePermission> updatedPermissions = permissions.stream().map(permission -> {
                Resource resource = storeFactory.getResourceStore().findById(permission.getResource().getId(), resourceServer.getId());
                return new ResourcePermission(resource, permission.getScopes(), permission.getResourceServer());
            }).collect(Collectors.toList());

            authorizationProvider.evaluators().from(updatedPermissions, createEvaluationContext(accessToken, clientConnection, authorizationProvider)).evaluate(new Decision<DefaultEvaluation>() {
                @Override
                public void onDecision(DefaultEvaluation evaluation) {
                    evaluations.put(evaluation.getPolicy().getId(), evaluation);
                }

                @Override
                public void onError(Throwable cause) {
                    throw new RuntimeException("Permission evaluation failed.", cause);
                }
            });
        });

        return evaluations;
    }

    private KeycloakEvaluationContext createEvaluationContext(AccessToken accessToken, ClientConnection clientConnection, AuthorizationProvider authorizationProvider) {
        KeycloakSession keycloakSession = authorizationProvider.getKeycloakSession();

        keycloakSession.getContext().setConnection(clientConnection);

        keycloakSession.getContext().setClient(getClientByClientId("photoz-html5-client"));

        ResteasyProviderFactory.pushContext(HttpHeaders.class, createHttpHeaders());

        KeycloakIdentity identity = new KeycloakIdentity(accessToken, keycloakSession);

        return new KeycloakEvaluationContext(identity, keycloakSession);
    }

    protected AccessToken createAccessToken(Set<String> roles) {
        AccessToken accessToken = new AccessToken();

        accessToken.setRealmAccess(new AccessToken.Access());
        accessToken.getRealmAccess().roles(roles);

        return accessToken;
    }


    private HttpHeaders createHttpHeaders() {
        return new HttpHeaders() {
            @Override
            public List<String> getRequestHeader(String name) {
                return null;
            }

            @Override
            public String getHeaderString(String name) {
                return null;
            }

            @Override
            public MultivaluedMap<String, String> getRequestHeaders() {
                return null;
            }

            @Override
            public List<MediaType> getAcceptableMediaTypes() {
                return null;
            }

            @Override
            public List<Locale> getAcceptableLanguages() {
                return null;
            }

            @Override
            public MediaType getMediaType() {
                return null;
            }

            @Override
            public Locale getLanguage() {
                return null;
            }

            @Override
            public Map<String, Cookie> getCookies() {
                return null;
            }

            @Override
            public Date getDate() {
                return null;
            }

            @Override
            public int getLength() {
                return 0;
            }
        };
    }

    protected ClientConnection createClientConnection(String remoteAddr) {
        return new ClientConnection() {
            @Override
            public String getRemoteAddr() {
                return remoteAddr;
            }

            @Override
            public String getRemoteHost() {
                return "localhost";
            }

            @Override
            public int getRemotePort() {
                return 0;
            }

            @Override
            public String getLocalAddr() {
                return null;
            }

            @Override
            public int getLocalPort() {
                return 0;
            }
        };
    }

    protected Invocation.Builder newPermissionRequest(String... id) {
        String idPathParam = "";

        if (id.length != 0) {
            idPathParam = "/" + id[0];
        }

        return newClient(getClientByClientId("photoz-restful-api"), "/resource-server/policy" + idPathParam);
    }

    private Policy createAdministrationPolicy() {
        return onAuthorizationSession(authorizationProvider -> {
            StoreFactory storeFactory = authorizationProvider.getStoreFactory();
            PolicyStore policyStore = storeFactory.getPolicyStore();
            Policy policy = policyStore.create("Administration Policy", "aggregate", resourceServer);

            policy.addAssociatedPolicy(anyAdminPolicy);
            policy.addAssociatedPolicy(onlyFromSpecificAddressPolicy);

            return policy;
        });
    }

    private Policy createOnlyFromSpecificAddressPolicy() {
        return onAuthorizationSession(authorizationProvider -> {
            StoreFactory storeFactory = authorizationProvider.getStoreFactory();
            PolicyStore policyStore = storeFactory.getPolicyStore();
            Policy policy = policyStore.create("Only From a Specific Client Address", "js", resourceServer);
            HashedMap config = new HashedMap();

            config.put("code",
                    "var contextAttributes = $evaluation.getContext().getAttributes();" +
                    "var networkAddress = contextAttributes.getValue('kc.client.network.ip_address');" +
                    "if ('127.0.0.1'.equals(networkAddress.asInetAddress(0).getHostAddress())) {" +
                        "$evaluation.grant();" +
                    "}");

            policy.setConfig(config);

            return policy;
        });
    }

    private Policy createAnyAdminPolicy() {
        return onAuthorizationSession(authorizationProvider -> {
            StoreFactory storeFactory = authorizationProvider.getStoreFactory();
            PolicyStore policyStore = storeFactory.getPolicyStore();
            Policy policy = policyStore.create("Any Admin Policy", "role", resourceServer);
            HashedMap config = new HashedMap();
            RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME);
            RoleModel adminRole = realm.getRole("admin");

            Map role = new HashMap();

            role.put("id", adminRole.getId());

            try {
                config.put("roles", JsonSerialization.writeValueAsString(new Map[] {role}));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            policy.setConfig(config);

            return policy;
        });
    }

    private Resource createAdminAlbumResource() {
        ResourceRepresentation representation = new ResourceRepresentation();

        representation.setName("Admin Resources");
        representation.setType("http://photoz.com/admin");
        representation.setUri("/admin/*");

        HashSet<ScopeRepresentation> scopes = new HashSet<>();

        scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:admin:manage"));

        representation.setScopes(scopes);

        return createResource(representation);
    }

    private Resource createAlbumResource() {
        ResourceRepresentation representation = new ResourceRepresentation();

        representation.setName("Album Resource");
        representation.setType("http://photoz.com/album");
        representation.setUri("/album/*");

        HashSet<ScopeRepresentation> scopes = new HashSet<>();

        scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:view"));
        scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:create"));
        scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:delete"));

        representation.setScopes(scopes);

        return createResource(representation);
    }

    protected Resource createResource(ResourceRepresentation representation) {
        return onAuthorizationSession(authorizationProvider -> {
            StoreFactory storeFactory = authorizationProvider.getStoreFactory();
            ScopeStore scopeStore = storeFactory.getScopeStore();

            representation.getScopes().forEach(scopeRepresentation -> {
                scopeStore.create(scopeRepresentation.getName(), resourceServer);
            });

            ResourceStore resourceStore = storeFactory.getResourceStore();
            Resource albumResource = resourceStore.create(representation.getName(), resourceServer, resourceServer.getId());

            albumResource.setType(representation.getType());
            albumResource.setUri(representation.getUri());
            albumResource.setIconUri(representation.getIconUri());

            return albumResource;
        });
    }

    private Policy createAnyUserPolicy() {
        return onAuthorizationSession(authorizationProvider -> {
            StoreFactory storeFactory = authorizationProvider.getStoreFactory();
            PolicyStore policyStore = storeFactory.getPolicyStore();
            Policy policy = policyStore.create("Any User Policy", "role", resourceServer);
            HashedMap config = new HashedMap();
            RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME);
            RoleModel userRole = realm.getRole("user");

            Map role = new HashMap();

            role.put("id", userRole.getId());

            try {
                config.put("roles", JsonSerialization.writeValueAsString(new Map[] {role}));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            policy.setConfig(config);

            return policy;
        });
    }
}
