package org.keycloak.testsuite.admin.group;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

import org.hamcrest.Matchers;
import org.jboss.arquillian.container.test.api.ContainerController;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.GroupResource;
import org.keycloak.admin.client.resource.GroupsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.GroupProvider;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.containers.KeycloakQuarkusServerDeployableContainer;
import org.keycloak.testsuite.updaters.Creator;
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
import org.wildfly.extras.creaper.core.online.operations.admin.Administration;

@AuthServerContainerExclude(REMOTE)
public class GroupSearchTest extends AbstractGroupTest {
    @ArquillianResource
    protected ContainerController controller;

    private static final String GROUP1 = "group1";
    private static final String GROUP2 = "group2";
    private static final String GROUP3 = "group3";

    private static final String PARENT_GROUP = "parentGroup";

    private static final String CHILD_GROUP = "childGroup";

    private static final String ATTR_ORG_NAME = "org";
    private static final String ATTR_ORG_VAL = "Test_\"organisation\"";
    private static final String ATTR_URL_NAME = "url";
    private static final String ATTR_URL_VAL = "https://foo.bar/clflds";
    private static final String ATTR_FILTERED_NAME = "filtered";
    private static final String ATTR_FILTERED_VAL = "does_not_matter";

    private static final String ATTR_QUOTES_NAME = "test \"123\"";

    private static final String ATTR_QUOTES_NAME_ESCAPED = "\"test \\\"123\\\"\"";
    private static final String ATTR_QUOTES_VAL = "field=\"blah blah\"";
    private static final String ATTR_QUOTES_VAL_ESCAPED = "\"field=\\\"blah blah\\\"\"";

    private static final String SEARCHABLE_ATTRS_PROP = "keycloak.group.searchableAttributes";

    GroupRepresentation group1;
    GroupRepresentation group2;
    GroupRepresentation group3;
    GroupRepresentation parentGroup;
    GroupRepresentation childGroup;

    @Before
    public void init() {
        group1 = new GroupRepresentation();
        group2 = new GroupRepresentation();
        group3 = new GroupRepresentation();
        parentGroup = new GroupRepresentation();
        childGroup = new GroupRepresentation();

        HashMap<String, List<String>> group1Attributes = new HashMap<>();
        group1Attributes.put(ATTR_ORG_NAME, Collections.singletonList(ATTR_ORG_VAL));
        group1Attributes.put(ATTR_URL_NAME, Collections.singletonList(ATTR_URL_VAL));
        group1.setAttributes(group1Attributes);

        HashMap<String, List<String>> group2Attributes = new HashMap<>();
        group2Attributes.put(ATTR_FILTERED_NAME, Collections.singletonList(ATTR_FILTERED_VAL));
        group2Attributes.put(ATTR_URL_NAME, Collections.singletonList(ATTR_URL_VAL));
        group2.setAttributes(group2Attributes);

        HashMap<String, List<String>> group3Attributes = new HashMap<>();
        group3Attributes.put(ATTR_ORG_NAME, Collections.singletonList("fake group"));
        group3Attributes.put(ATTR_QUOTES_NAME, Collections.singletonList(ATTR_QUOTES_VAL));
        group3.setAttributes(group3Attributes);

        HashMap<String, List<String>> childAttributes = new HashMap<>();
        childAttributes.put(ATTR_ORG_NAME, Collections.singletonList("childOrg"));
        childGroup.setAttributes(childAttributes);

        HashMap<String, List<String>> parentAttributes = new HashMap<>();
        parentAttributes.put(ATTR_ORG_NAME, Collections.singletonList("parentOrg"));
        parentGroup.setAttributes(parentAttributes);

        group1.setName(GROUP1);
        group2.setName(GROUP2);
        group3.setName(GROUP3);
        parentGroup.setName(PARENT_GROUP);
        childGroup.setName(CHILD_GROUP);
    }

    public RealmResource testRealmResource() {
        return adminClient.realm(TEST);
    }

    @Test
    public void testQuerySearch() throws Exception {
        configureSearchableAttributes(ATTR_URL_NAME, ATTR_ORG_NAME, ATTR_QUOTES_NAME);
        try (Creator<GroupResource> groupCreator1 = Creator.create(testRealmResource(), group1);
             Creator<GroupResource> groupCreator2 = Creator.create(testRealmResource(), group2);
             Creator<GroupResource> groupCreator3 = Creator.create(testRealmResource(), group3)) {
            search(String.format("%s:%s", ATTR_ORG_NAME, ATTR_ORG_VAL), GROUP1);
            search(String.format("%s:%s", ATTR_URL_NAME, ATTR_URL_VAL), GROUP1, GROUP2);
            search(String.format("%s:%s %s:%s", ATTR_ORG_NAME, ATTR_ORG_VAL, ATTR_URL_NAME, ATTR_URL_VAL),
                    GROUP1);
            search(String.format("%s:%s %s:%s", ATTR_ORG_NAME, "wrong val", ATTR_URL_NAME, ATTR_URL_VAL));
            search(String.format("%s:%s", ATTR_QUOTES_NAME_ESCAPED, ATTR_QUOTES_VAL_ESCAPED), GROUP3);

            // "filtered" attribute won't take effect when JPA is used
            String[] expectedRes = new String[]{GROUP1, GROUP2};
            search(String.format("%s:%s %s:%s", ATTR_URL_NAME, ATTR_URL_VAL, ATTR_FILTERED_NAME, ATTR_FILTERED_VAL), expectedRes);
        } finally {
            resetSearchableAttributes();
        }
    }

    @Test
    public void testNestedGroupQuerySearch() throws Exception {
        configureSearchableAttributes(ATTR_URL_NAME, ATTR_ORG_NAME, ATTR_QUOTES_NAME);
        try (Creator<GroupResource> parentGroupCreator = Creator.create(testRealmResource(), parentGroup)) {
            parentGroupCreator.resource().subGroup(childGroup);
            // query for the child group by org name
            GroupsResource search = testRealmResource().groups();
            String searchQuery = String.format("%s:%s", ATTR_ORG_NAME, "childOrg");
            List<GroupRepresentation> found = search.query(searchQuery);

            assertThat(found.size(), is(1));
            assertThat(found.get(0).getName(), is(equalTo(PARENT_GROUP)));

            List<GroupRepresentation> subGroups = found.get(0).getSubGroups();
            assertThat(subGroups.size(), is(1));
            assertThat(subGroups.get(0).getName(), is(equalTo(CHILD_GROUP)));
        } finally {
            resetSearchableAttributes();
        }
    }

    private void search(String searchQuery, String... expectedGroupIds) {
        GroupsResource search = testRealmResource().groups();
        List<String> found = search.query(searchQuery).stream()
                .map(GroupRepresentation::getName)
                .collect(Collectors.toList());
        assertThat(found, containsInAnyOrder(expectedGroupIds));
    }

    void configureSearchableAttributes(String... searchableAttributes) throws Exception {
        log.infov("Configuring searchableAttributes");
        if (suiteContext.getAuthServerInfo().isUndertow()) {
            controller.stop(suiteContext.getAuthServerInfo().getQualifier());
            System.setProperty(SEARCHABLE_ATTRS_PROP, String.join(",", searchableAttributes));
            controller.start(suiteContext.getAuthServerInfo().getQualifier());
        } else if (suiteContext.getAuthServerInfo().isJBossBased()) {
            searchableAttributes = Arrays.stream(searchableAttributes).map(a -> a.replace("\"", "\\\\\\\"")).toArray(String[]::new);
            String s = "\\\"" + String.join("\\\",\\\"", searchableAttributes) + "\\\"";
            executeCli("/subsystem=keycloak-server/spi=group:add()",
                    "/subsystem=keycloak-server/spi=group/provider=jpa/:add(properties={searchableAttributes => \"[" + s + "]\"},enabled=true)");
        } else if (suiteContext.getAuthServerInfo().isQuarkus()) {
            searchableAttributes = Arrays.stream(searchableAttributes)
                    .map(a -> a.replace(" ", "\\ ").replace("\"", "\\\\\\\""))
                    .toArray(String[]::new);
            String s = String.join(",", searchableAttributes);
            controller.stop(suiteContext.getAuthServerInfo().getQualifier());
            KeycloakQuarkusServerDeployableContainer container = (KeycloakQuarkusServerDeployableContainer) suiteContext.getAuthServerInfo().getArquillianContainer().getDeployableContainer();
            container.setAdditionalBuildArgs(
                    Collections.singletonList("--spi-group-jpa-searchable-attributes=\"" + s + "\""));
            controller.start(suiteContext.getAuthServerInfo().getQualifier());
        } else {
            throw new RuntimeException("Don't know how to config");
        }
        reconnectAdminClient();
    }

    void resetSearchableAttributes() throws Exception {
        log.info("Reset searchableAttributes");

        if (suiteContext.getAuthServerInfo().isUndertow()) {
            controller.stop(suiteContext.getAuthServerInfo().getQualifier());
            System.clearProperty(SEARCHABLE_ATTRS_PROP);
            controller.start(suiteContext.getAuthServerInfo().getQualifier());
        } else if (suiteContext.getAuthServerInfo().isJBossBased()) {
            executeCli("/subsystem=keycloak-server/spi=group:remove");
        } else if (suiteContext.getAuthServerInfo().isQuarkus()) {
            KeycloakQuarkusServerDeployableContainer container = (KeycloakQuarkusServerDeployableContainer) suiteContext.getAuthServerInfo().getArquillianContainer().getDeployableContainer();
            container.setAdditionalBuildArgs(Collections.emptyList());
            container.restartServer();
        } else {
            throw new RuntimeException("Don't know how to config");
        }
        reconnectAdminClient();
    }

    private void executeCli(String... commands) throws Exception {
        OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
        Administration administration = new Administration(client);

        log.debug("Running CLI commands:");
        for (String c : commands) {
            log.debug(c);
            client.execute(c).assertSuccess();
        }
        log.debug("Done");

        administration.reload();

        client.close();
    }

    @Override
    public void addTestRealms(List<RealmRepresentation> testRealms) {
        loadTestRealm(testRealmReps);
    }
}
