/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.jackrabbit.oak.security.user;

import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import javax.jcr.AccessDeniedException;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;

import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.QueryEngine;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager;
import org.apache.jackrabbit.oak.InitialContent;
import org.apache.jackrabbit.oak.InitialContentHelper;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
import org.apache.jackrabbit.oak.spi.security.user.AuthorizableNodeName;
import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil;
import org.apache.jackrabbit.util.Text;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.NT_REP_AUTHORIZABLE_FOLDER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * @since OAK 1.0
 */
public class UserProviderTest {

    private Root root;

    private ConfigurationParameters defaultConfig;
    private String defaultUserPath;
    private String defaultGroupPath;
    
    private Map<String, Object> customOptions;
    private final String customUserPath = "/home/users";
    private final String customGroupPath = "/home/groups";

    @Before
    public void setUp() throws Exception {
        root = new Oak(new MemoryNodeStore(InitialContentHelper.INITIAL_CONTENT))
                .with(new OpenSecurityProvider())
                .with(new InitialContent())
                .with(new PropertyIndexEditorProvider())
                .createRoot();

        defaultConfig = ConfigurationParameters.EMPTY;
        defaultUserPath = defaultConfig.getConfigValue(UserConstants.PARAM_USER_PATH, UserConstants.DEFAULT_USER_PATH);
        defaultGroupPath = defaultConfig.getConfigValue(UserConstants.PARAM_GROUP_PATH, UserConstants.DEFAULT_GROUP_PATH);

        customOptions = new HashMap<>();
        customOptions.put(UserConstants.PARAM_GROUP_PATH, customGroupPath);
        customOptions.put(UserConstants.PARAM_USER_PATH, customUserPath);
    }

    @After
    public void tearDown() {
        root.refresh();
        root = null;
    }

    private UserProvider createUserProvider() {
        return new UserProvider(root, defaultConfig);
    }

    private UserProvider createUserProvider(int defaultDepth) {
        Map<String, Object> options = new HashMap<>(customOptions);
        options.put(UserConstants.PARAM_DEFAULT_DEPTH, defaultDepth);
        return new UserProvider(root, ConfigurationParameters.of(options));
    }

    private UserProvider createUserProviderRFC7612() {
        Map<String, Object> options = new HashMap<>(customOptions);
        options.put(UserConstants.PARAM_ENABLE_RFC7613_USERCASE_MAPPED_PROFILE, true);
        return new UserProvider(root, ConfigurationParameters.of(options));
    }

    @Test
    public void testCreateUser() throws Exception {
        UserProvider up = createUserProvider();

        // create test user
        Tree userTree = up.createUser("user1", null);

        assertNotNull(userTree);
        assertTrue(Text.isDescendant(defaultUserPath, userTree.getPath()));
        int level = defaultConfig.getConfigValue(UserConstants.PARAM_DEFAULT_DEPTH, UserConstants.DEFAULT_DEPTH) + 1;
        assertEquals(defaultUserPath, Text.getRelativeParent(userTree.getPath(), level));
        
        // make sure all users are created in a structure with default depth
        userTree = up.createUser("b", null);
        assertEquals(defaultUserPath + "/b/bb/b", userTree.getPath());

        Map<String, String> m = new HashMap<>();
        m.put("bb",     "/b/bb/bb");
        m.put("bbb",    "/b/bb/bbb");
        m.put("bbbb",   "/b/bb/bbbb");
        m.put("bh",     "/b/bh/bh");
        m.put("bHbh",   "/b/bH/bHbh");
        m.put("b_Hb",   "/b/b_/b_Hb");
        m.put("basim", "/b/ba/basim");

        for (Map.Entry<String, String> entry : m.entrySet()) {
            userTree = up.createUser(entry.getKey(), null);
            assertEquals(defaultUserPath + entry.getValue(), userTree.getPath());
        }
    }

    @Test
    public void testCreateUserWithPath() throws Exception {
        UserProvider up = createUserProvider(1);

        // create test user
        Tree userTree = up.createUser("nadine", "a/b/c");
        assertNotNull(userTree);
        assertTrue(Text.isDescendant(customUserPath, userTree.getPath()));
        String userPath = customUserPath + "/a/b/c/nadine";
        assertEquals(userPath, userTree.getPath());
    }

    @Test(expected = AccessDeniedException.class)
    public void testCreateUserMissingAccessOnFolders() throws RepositoryException {
        Tree t = mock(Tree.class);
        when(t.getParent()).thenReturn(t);
        when(t.exists()).thenReturn(false);
        when(t.isRoot()).thenReturn(false, false, true);
        Root r = when(mock(Root.class).getTree(anyString())).thenReturn(t).getMock();

        UserProvider up = new UserProvider(r, ConfigurationParameters.EMPTY);
        up.createUser("uid", null);
    }

    @Test
    public void testCreateGroup() throws RepositoryException {
        UserProvider up = createUserProvider();

        Tree groupTree = up.createGroup("group1", null);

        assertNotNull(groupTree);
        assertTrue(Text.isDescendant(defaultGroupPath, groupTree.getPath()));

        int level = defaultConfig.getConfigValue(UserConstants.PARAM_DEFAULT_DEPTH, UserConstants.DEFAULT_DEPTH) + 1;
        assertEquals(defaultGroupPath, Text.getRelativeParent(groupTree.getPath(), level));
    }

    @Test
    public void testCreateGroupWithPath() throws Exception {
        UserProvider up = createUserProvider(4);

        // create test user
        Tree group = up.createGroup("authors", "a/b/c");
        assertNotNull(group);
        assertTrue(Text.isDescendant(customGroupPath, group.getPath()));
        String groupPath = customGroupPath + "/a/b/c/authors";
        assertEquals(groupPath, group.getPath());
    }

    @Test
    public void testCreateWithCustomDepth() throws Exception {
        UserProvider userProvider = createUserProvider(3);

        Tree userTree = userProvider.createUser("b", null);
        assertEquals(customUserPath + "/b/bb/bbb/b", userTree.getPath());

        Map<String, String> m = new HashMap<>();
        m.put("bb",     "/b/bb/bbb/bb");
        m.put("bbb",    "/b/bb/bbb/bbb");
        m.put("bbbb",   "/b/bb/bbb/bbbb");
        m.put("bL",     "/b/bL/bLL/bL");
        m.put("bLbh",   "/b/bL/bLb/bLbh");
        m.put("b_Lb",   "/b/b_/b_L/b_Lb");
        m.put("basiL",  "/b/ba/bas/basiL");

        for (Map.Entry<String, String> entry : m.entrySet()) {
            userTree = userProvider.createUser(entry.getKey(), null);
            assertEquals(customUserPath + entry.getValue(), userTree.getPath());
        }
    }

    @Test
    public void testCreateWithCollision() throws Exception {
        UserProvider userProvider = createUserProvider();

        userProvider.createUser("AmaLia", null);

        Map<String, String> colliding = new HashMap<>();
        colliding.put("AmaLia", null);
        colliding.put("AmalIa", "s/ome/path");
        colliding.put("amalia", null);
        colliding.put("Amalia", "a/b/c");

        for (Map.Entry<String, String> entry : colliding.entrySet()) {
            try {
                userProvider.createUser(entry.getKey(), entry.getValue());
                root.commit();
                fail("userID collision must be detected");
            } catch (CommitFailedException e) {
                // success
            }
        }

        for (Map.Entry<String, String> entry : colliding.entrySet()) {
            try {
                userProvider.createGroup(entry.getKey(), entry.getValue());
                root.commit();
                fail("userID collision must be detected");
            } catch (CommitFailedException e) {
                // success
            }
        }
    }

    @Test
    public void testCreateWithCollidingFolder() throws Exception {
        Tree ut = mock(Tree.class);

        Tree t = mock(Tree.class);
        when(t.getParent()).thenReturn(t);
        when(t.exists()).thenReturn(true);
        when(t.isRoot()).thenReturn(true);
        when(t.hasChild("uid")).thenReturn(true, false);
        when(t.addChild("uid")).thenReturn(ut);
        when(t.getChild(anyString())).thenReturn(t);
        when(t.getPath()).thenReturn(UserConstants.DEFAULT_USER_PATH);
        when(t.getProperty(JCR_PRIMARYTYPE)).thenReturn(PropertyStates.createProperty(JCR_PRIMARYTYPE, NT_REP_AUTHORIZABLE_FOLDER, Type.NAME));

        Root r = when(mock(Root.class).getTree(anyString())).thenReturn(t).getMock();
        when(r.getContentSession()).thenReturn(root.getContentSession());

        UserProvider up = new UserProvider(r, ConfigurationParameters.EMPTY);
        Tree tree = up.createUser("uid", null);
        assertSame(ut, tree);
    }

    @Test
    public void testCreateUserRFC7613Disabled() throws Exception {
        String userHalfWidth = "Amalia";
        String userFullWidth = "\uff21\uff4d\uff41\uff4c\uff49\uff41";

        UserProvider userProvider = createUserProvider();

        Tree userTreeHalfWidth = userProvider.createUser(userHalfWidth, null);
        Tree userTreeFullWidth = userProvider.createUser(userFullWidth, null);

        root.commit();

        assertEquals(userHalfWidth, UserUtil.getAuthorizableId(userTreeHalfWidth));
        assertEquals(userFullWidth, UserUtil.getAuthorizableId(userTreeFullWidth));
    }

    @Test
    public void testCreateUserRFC7613Enabled() throws Exception {
        String userHalfWidth = "Amalia";
        String userFullWidth = "\uff21\uff4d\uff41\uff4c\uff49\uff41";

        UserProvider userProvider = createUserProviderRFC7612();

        userProvider.createUser(userHalfWidth, null);

        try {
            userProvider.createUser(userFullWidth, null);
            root.commit();
            fail("userID collision must be detected");
        } catch (CommitFailedException e) {
            // success
        }
    }

    @Test
    public void testIllegalChars() throws Exception {
        UserProvider userProvider = createUserProvider();

        Map<String, String> m = new HashMap<>();
        m.put("z[x]", "/z/" + Text.escapeIllegalJcrChars("z[") + '/' + Text.escapeIllegalJcrChars("z[x]"));
        m.put("z*x", "/z/" + Text.escapeIllegalJcrChars("z*") + '/' + Text.escapeIllegalJcrChars("z*x"));
        m.put("z/x", "/z/" + Text.escapeIllegalJcrChars("z/") + '/' + Text.escapeIllegalJcrChars("z/x"));
        m.put("%\r|", '/' +Text.escapeIllegalJcrChars("%")+ '/' + Text.escapeIllegalJcrChars("%\r") + '/' + Text.escapeIllegalJcrChars("%\r|"));

        for (Map.Entry<String, String> entry : m.entrySet()) {
            String uid = entry.getKey();
            Tree user = userProvider.createUser(uid, null);
            root.commit();

            assertEquals(defaultUserPath + entry.getValue(), user.getPath());
            assertEquals(uid, UserUtil.getAuthorizableId(user));

            Tree ath = userProvider.getAuthorizable(uid);
            assertNotNull("Tree with id " + uid + " must exist.", ath);
        }
    }

    @Test
    public void testGetAuthorizable() throws Exception {
        UserProvider up = createUserProvider();

        String userID = "hannah";
        String groupID = "cLevel";

        Tree user = up.createUser(userID, null);
        Tree group = up.createGroup(groupID, null);
        root.commit();

        Tree a = up.getAuthorizable(userID);
        assertNotNull(a);
        assertEquals(user.getPath(), a.getPath());

        a = up.getAuthorizable(groupID);
        assertNotNull(a);
        assertEquals(group.getPath(), a.getPath());
    }

    @Test
    public void testGetAuthorizableByPath() throws Exception {
        UserProvider up = createUserProvider();

        Tree user = up.createUser("shams", null);
        Tree a = up.getAuthorizableByPath(user.getPath());
        assertNotNull(a);
        assertEquals(user.getPath(), a.getPath());

        Tree group = up.createGroup("devs", null);
        a = up.getAuthorizableByPath(group.getPath());
        assertNotNull(a);
        assertEquals(group.getPath(), a.getPath());
    }

    @Test
    public void testGetAuthorizableByPrincipal() throws Exception {
        UserProvider up = createUserProvider();
        up.createUser("uid", null);
        TreeBasedPrincipal tbp = new TreeBasedPrincipal("uid", "/path", NamePathMapper.DEFAULT) {
            @Override
            @NotNull String getOakPath() {
                return "/path";
            }
        };
        // changes not yet persisted -> query returns no result.
        assertNotNull(up.getAuthorizableByPrincipal(tbp));
    }

    @Test
    public void testGetAuthorizableByPrincipal2() throws Exception {
        UserProvider up = createUserProvider();
        up.createUser("uid", null);
        TreeBasedPrincipal tbp = new TreeBasedPrincipal("uid", "/path", NamePathMapper.DEFAULT) {
            @Override
            @NotNull String getOakPath() throws RepositoryException {
                throw new RepositoryException();
            }
        };
        // changes not yet persisted -> query returns no result.
        assertNull(up.getAuthorizableByPrincipal(tbp));
    }

    @Test
    public void testGetAuthorizableByPrincipalQueryFails() throws Exception {
        QueryEngine qe = mock(QueryEngine.class);
        when(qe.executeQuery(anyString(), anyString(), anyLong(), anyLong(), any(Map.class), any(Map.class))).thenThrow(new ParseException("err",0));
        Root r = when(mock(Root.class).getQueryEngine()).thenReturn(qe).getMock();
        UserProvider up = new UserProvider(r, ConfigurationParameters.EMPTY);
        assertNull(up.getAuthorizableByPrincipal(new PrincipalImpl("name")));
    }

    @Test
    public void testGetAuthorizableId() throws Exception {
        UserProvider up = createUserProvider();

        String userID = "Amanda";
        Tree user = up.createUser(userID, null);
        assertEquals(userID, UserUtil.getAuthorizableId(user));

        String groupID = "visitors";
        Tree group = up.createGroup(groupID, null);
        assertEquals(groupID, UserUtil.getAuthorizableId(group));
    }

    @Test
    public void testRemoveParentTree() throws Exception {
        UserProvider up = createUserProvider();
        Tree u1 = up.createUser("b", "b");
        Tree u2 = up.createUser("bb", "bb");

        Tree folder = root.getTree(Text.getRelativeParent(u1.getPath(), 2));
        folder.remove();
        if (up.getAuthorizable("b") != null) {
            fail("Removing the top authorizable folder must remove all users contained.");
            u1.remove();
        }
        if (up.getAuthorizable("bb") != null) {
            fail("Removing the top authorizable folder must remove all users contained.");
            u2.remove();
        }
    }

    @Test
    public void testCollisions() throws Exception {
        ConfigurationParameters config = ConfigurationParameters.of(UserConstants.PARAM_AUTHORIZABLE_NODE_NAME, (AuthorizableNodeName) authorizableId -> "aaa");
        UserProvider up = new UserProvider(root, config);

        try {
            Tree u1 = up.createUser("a", null);
            assertEquals("aaa", u1.getName());
            Tree u2 = up.createUser("b", null);
            assertEquals("aaa1", u2.getName());
            Tree u3 = up.createUser("c", null);
            assertEquals("aaa2", u3.getName());
            Tree u4 = up.createUser("d", null);
            assertEquals("aaa3", u4.getName());
        } finally {
            root.refresh();
        }
    }

    @Test
    public void testAutoCreatedItemsUponUserCreation() throws Exception {
        UserProvider up = createUserProvider();
        assertAutoCreatedItems(up.createUser("c", null), UserConstants.NT_REP_USER, root);
    }

    @Test
    public void testAutoCreatedItemsUponSystemUserCreation() throws Exception {
        UserProvider up = createUserProvider();
        assertAutoCreatedItems(up.createSystemUser("s", null), UserConstants.NT_REP_SYSTEM_USER, root);
    }

    @Test
    public void testAutoCreatedItemsUponGroupCreation() throws Exception {
        UserProvider up = createUserProvider();
        assertAutoCreatedItems(up.createGroup("g", null), UserConstants.NT_REP_GROUP, root);
    }

    private static void assertAutoCreatedItems(@NotNull Tree authorizableTree, @NotNull String ntName, @NotNull Root root) throws Exception {
        NodeType repUser = ReadOnlyNodeTypeManager.getInstance(root, NamePathMapper.DEFAULT).getNodeType(ntName);
        for (NodeDefinition cnd : repUser.getChildNodeDefinitions()) {
            if (cnd.isAutoCreated()) {
                assertTrue(authorizableTree.hasChild(cnd.getName()));
            }
        }

        for (PropertyDefinition pd : repUser.getPropertyDefinitions()) {
            if (pd.isAutoCreated()) {
                assertTrue(authorizableTree.hasProperty(pd.getName()));
            }
        }
    }
}
