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

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.security.Principal;
import java.util.HashSet;
import java.util.Iterator;

import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import org.apache.catalina.User;
import org.apache.tomcat.util.file.ConfigFileLoader;
import org.apache.tomcat.util.file.ConfigurationSource;

public class MemoryUserDatabaseTests {
    private static File TEST_FILE = new File(System.getProperty("java.io.tmpdir"), "tomcat-users.xml");

    private static MemoryUserDatabase db;

    @BeforeClass
    public static void createSampleDB()
        throws Exception {

        try(BufferedWriter out = new BufferedWriter(new FileWriter(TEST_FILE))) {
            out.write("<?xml version=\"1.0\" ?>"
                    + "<tomcat-users xmlns=\"http://tomcat.apache.org/xml\""
                    + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                    + " xsi:schemaLocation=\"http://tomcat.apache.org/xml/tomcat-users.xsd\""
                    + " version=\"1.0\">"
                    + "<role rolename=\"testrole\" />"
                    + "<group groupname=\"testgroup\" />"
                    + "<user username=\"admin\" password=\"sekr3t\" roles=\"testrole, otherrole\" groups=\"testgroup, othergroup\" />"
                    + "</tomcat-users>");
        }

        // MemoryUserDatabase requires the use of ConfigFileLoader/ConfigurationSource
        ConfigFileLoader.setSource(new ConfigurationSource() {
            protected final File userDir = new File(System.getProperty("java.io.tmpdir"));
            protected final URI userDirUri = userDir.toURI();
            @Override
            public Resource getResource(String name) throws IOException {
                File f = new File(name);
                if (!f.isAbsolute()) {
                    f = new File(userDir, name);
                }
                if (f.isFile()) {
                    return new Resource(new FileInputStream(f), f.toURI());
                } else {
                    throw new FileNotFoundException(name);
                }
            }
            @Override
            public URI getURI(String name) {
                File f = new File(name);
                if (!f.isAbsolute()) {
                    f = new File(userDir, name);
                }
                if (f.isFile()) {
                    return f.toURI();
                }
                return userDirUri.resolve(name);
            }
        });

        db = new MemoryUserDatabase();
        db.setPathname(TEST_FILE.getAbsolutePath());
        db.open();
    }

    @AfterClass
    public static void cleanup() {
        Assert.assertTrue(TEST_FILE.delete());
    }

    @Test
    public void testLoadUserDatabase()
        throws Exception {
        assertPrincipalNames(new String[] { "testrole", "otherrole"}, db.getRoles());
        assertPrincipalNames(new String[] { "testgroup", "othergroup"}, db.getGroups());

        Iterator<User> users = db.getUsers();

        Assert.assertTrue("No users found", users.hasNext());

        User user = users.next();

        Assert.assertEquals("admin", user.getName());
        Assert.assertNull(user.getFullName());
        Assert.assertEquals("sekr3t", user.getPassword());

        assertPrincipalNames(new String[] { "testrole", "otherrole"}, user.getRoles());
        assertPrincipalNames(new String[] { "testgroup", "othergroup"}, user.getGroups());
    }

    public void testReloadUserDatabase()
        throws Exception {
        // Change the database on the disk and reload

        try(BufferedWriter out = new BufferedWriter(new FileWriter(TEST_FILE))) {
            out.write("<?xml version=\"1.0\" ?>"
                    + "<tomcat-users xmlns=\"http://tomcat.apache.org/xml\""
                    + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                    + " xsi:schemaLocation=\"http://tomcat.apache.org/xml/tomcat-users.xsd\""
                    + " version=\"1.0\">"
                    + "<role rolename=\"foo\" />"
                    + "<group groupname=\"bar\" />"
                    + "<user username=\"root\" password=\"sup3Rsekr3t\" roles=\"foo, bar\" groups=\"bar, foo\" />"
                    + "</tomcat-users>");

            db.open();
        }

        assertPrincipalNames(new String[] { "foo", "bar"}, db.getRoles());
        assertPrincipalNames(new String[] { "bar", "foo"}, db.getGroups());

        Iterator<User> users = db.getUsers();

        Assert.assertTrue("No users found", users.hasNext());

        User user = users.next();

        Assert.assertEquals("root", user.getName());
        Assert.assertNull(user.getFullName());
        Assert.assertEquals("sup3Rsekr3t", user.getPassword());

        assertPrincipalNames(new String[] { "foo", "bar"}, user.getRoles());
        assertPrincipalNames(new String[] { "bar", "foo"}, user.getGroups());
    }

    @Test
    public void testMultithreadedMutateUserDatabase()
        throws Exception {
        // Generate lots of concurrent load on the user database
        Runnable job = new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<10; ++i)
                    db.createUser("newUser-" + Thread.currentThread().getName() + "-" + i, "x", null);
            }
        };

        int numThreads = 100;
        Thread[] threads = new Thread[numThreads + 1];
        for(int i=0; i<numThreads; ++i)
            threads[i] = new Thread(job);

        // Let's
        threads[numThreads] = new Thread(new Runnable() {
            @Override
            public void run() {
                try { db.open(); }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        ++numThreads;

        for(int i=0; i<numThreads; ++i)
            threads[i].start();

        for(int i=0; i<numThreads; ++i)
            threads[i].join();

        // Remove all those extra users
        Iterator<User> users = db.getUsers();
        for(; users.hasNext();) {
            User user = users.next();
            if(user.getUsername().startsWith("newUser"))
                db.removeUser(user);
        }

        users = db.getUsers();

        Assert.assertTrue("No users found", users.hasNext());

        User user = users.next();

        Assert.assertEquals("admin", user.getName());
        Assert.assertNull(user.getFullName());
        Assert.assertEquals("sekr3t", user.getPassword());

        assertPrincipalNames(new String[] { "testrole", "otherrole"}, user.getRoles());
        assertPrincipalNames(new String[] { "testgroup", "othergroup"}, user.getGroups());
    }

    private void assertPrincipalNames(String[] expectedNames, Iterator<? extends Principal> i)
    {
        HashSet<String> names = new HashSet<>(expectedNames.length);
        for(String name : expectedNames)
            names.add(name);

        int j=0;
        while(i.hasNext()) {
            Assert.assertTrue(names.contains(i.next().getName()));
            j++;
        }

        Assert.assertEquals(expectedNames.length, j);
    }
}
