/*
 * 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.shiro.realm.text;

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.junit.Test;

import java.text.ParseException;
import java.util.Arrays;

import static org.junit.Assert.*;

public class TextConfigurationRealmTest {

    private TestRealm realm;

    private void setRoles() {
        StringBuilder roleDefinitions = new StringBuilder()
                .append("role1 = role1_permission1\n")
                .append("role2 = role2_permission1, role2_permission2\n");
        realm.setRoleDefinitions(roleDefinitions.toString());
    }

    private void setUsers() {
        StringBuilder userDefinitions = new StringBuilder();
        for (int i = 1; i < 3; i++) {
            userDefinitions.append(String.format("user%1$d = user%1$d_password, role1, role2%n", i));
        }
        realm.setUserDefinitions(userDefinitions.toString());
    }

    private void setUpForReadConfigurationTest() {
        realm = new TestRealm() {
            /*
             * Demonstrates that a lock can't be obtained on the realm by a read thread until after
             * the lock is released.
             */
            public void test(Thread runnable) throws InterruptedException {
                // Obtain the realm's locks
                USERS_LOCK.writeLock().lock();
                try {
                    ROLES_LOCK.writeLock().lock();
                    try {
                        // Any read threads attempting to obtain the realms lock will block
                        runnable.start();
                        Thread.sleep(500);
                        // Process role and user definitions
                        realm.onInit();

                    } finally {
                        ROLES_LOCK.writeLock().unlock();
                    }
                } finally {
                    USERS_LOCK.writeLock().unlock();
                }
            }
        };
        setRoles();
        setUsers();
    }

    /*
     * Executes a test that attempts to read to read from a realm before it is loaded.
     */
    private void executeTest(Runnable runnable) throws InterruptedException {
        TestThread testThread = new TestThread(runnable);
        Thread testTask = new Thread(testThread);
        realm.test(testTask);
        testTask.join(500);
        // Check whether any assertion error was thrown by the read thread
        testThread.test();
    }

    /*
     * Tests that roles and account can't be tested while the realm is being loaded. 
     */
    @Test
    public void testRoleAndUserAccount() throws InterruptedException {
        setUpForReadConfigurationTest();
        executeTest(new Runnable() {
            public void run() {
                assertTrue("role not found when it was expected", realm.roleExists("role1"));
                assertTrue("user not found when it was expected", realm.accountExists("user1"));
            }
        });
    }

    /*
     * Tests that roles can't be read while the realm is being loaded. 
     */
    @Test
    public void testHasRole() throws InterruptedException {
        setUpForReadConfigurationTest();
        executeTest(new Runnable() {
            public void run() {
                PrincipalCollection principalCollection = new SimplePrincipalCollection("user1", "realm1");
                assertTrue("principal doesn't have role when it should",
                        realm.hasRole(principalCollection, "role2"));
                assertTrue("principal doesn't have all roles when it should",
                        realm.hasAllRoles(principalCollection, Arrays.asList(new String[]{"role1", "role2"})));
            }
        });
    }

    /*
     * Tests that roles can't be checked while the realm is being loaded. 
     */
    @Test
    public void testCheckRole() throws InterruptedException {
        setUpForReadConfigurationTest();
        executeTest(new Runnable() {
            public void run() {
                PrincipalCollection principalCollection = new SimplePrincipalCollection("user1", "realm1");
                try {
                    realm.checkRoles(principalCollection, new String[]{"role1", "role2"});
                } catch (AuthorizationException ae) {
                    fail("principal doesn't have all roles when it should");
                }
            }
        });
    }

    /*
     * Tests that a principal's permissions can't be checked while the realm is being loaded. 
     */
    @Test
    public void testCheckPermission() throws InterruptedException {
        setUpForReadConfigurationTest();
        executeTest(new Runnable() {
            public void run() {
                PrincipalCollection principalCollection = new SimplePrincipalCollection("user1", "realm1");
                try {
                    realm.checkPermission(principalCollection, "role1_permission1");
                    realm.checkPermissions(principalCollection, new String[]{"role1_permission1", "role2_permission2"});
                } catch (AuthorizationException ae) {
                    fail("principal doesn't have permission when it should");
                }
            }
        });
    }

    /*
     * Tests that a principal's permissions can't be checked while the realm is being loaded. 
     */
    @Test
    public void testIsPermitted() throws InterruptedException {
        setUpForReadConfigurationTest();
        executeTest(new Runnable() {
            public void run() {
                PrincipalCollection principalCollection = new SimplePrincipalCollection("user1", "realm1");
                assertTrue("permission not permitted when it should be", realm.isPermitted(principalCollection, "role1_permission1"));
                assertTrue("permission not permitted when it should be",
                        realm.isPermittedAll(principalCollection, new String[]{"role1_permission1", "role2_permission2"}));
            }
        });
    }

    /*
     * Test that role definitions cannot be updated when a read thread holds the realm's lock.
     */
    @Test
    public void testProcessRoleDefinitions() throws InterruptedException {
        realm = new TestRealm() {
            public void test(Thread runnable) throws InterruptedException {
                // While the realm's lock is held by this thread role definitions cannot be processed
                // Obtain the realm's locks
                ROLES_LOCK.writeLock().lock();
                try {
                    runnable.start();
                    Thread.sleep(500);
                    // No role until lock is released and role definitions are processed
                    assertFalse("role exists when it shouldn't", realm.roleExists("role1"));
                } finally {
                    ROLES_LOCK.writeLock().unlock();
                }
            }
        };
        // A thread to process new role definitions
        TestThread testThread = new TestThread(new Runnable() {
            public void run() {
                try {
                    realm.processRoleDefinitions();
                } catch (ParseException e) {
                    fail("Unable to parse role definitions");
                }
            }
        });
        setRoles();
        Thread testTask = new Thread(testThread);
        realm.test(testTask);
        testTask.join(500);
        assertTrue("role doesn't exist when it should", realm.roleExists("role1"));
        testThread.test();
    }

    /*
     * Test that user definitions cannot be updated when a read thread holds the realm's lock.
     */
    @Test
    public void testProcessUserDefinitions() throws InterruptedException {
        realm = new TestRealm() {
            public void test(Thread runnable) throws InterruptedException {
                // While the realm's lock is held by this thread user definitions cannot be processed
                // Obtain the realm's locks
                USERS_LOCK.writeLock().lock();
                try {
                    runnable.start();
                    Thread.sleep(500);
                    // No account until lock is released and user definitions are processed
                    assertFalse("account exists when it shouldn't", realm.accountExists("user1"));
                } finally {
                    USERS_LOCK.writeLock().unlock();
                }
            }
        };
        TestThread testThread = new TestThread(new Runnable() {
            public void run() {
                try {
                    realm.processUserDefinitions();
                } catch (ParseException e) {
                    fail("Unable to parse user definitions");
                }
            }
        });
        setUsers();
        Thread testTask = new Thread(testThread);
        realm.test(testTask);
        testTask.join(500);
        assertTrue("account doesn't exist when it should", realm.accountExists("user1"));
        testThread.test();
    }

    /*
     * A Class that captures a thread's assertion error.
     */
    private class TestThread implements Runnable {
        private Runnable test;
        private volatile AssertionError ae;

        public TestThread(Runnable test) {
            this.test = test;
        }

        public void run() {
            try {
                test.run();
            } catch (AssertionError ae) {
                this.ae = ae;
            }
        }

        public void test() {
            if (ae != null)
                throw ae;
        }
    }

    /*
     * Provides an additional method that has access to the realm's lock for mutual exclusion.
     */
    private abstract class TestRealm extends TextConfigurationRealm {
        abstract public void test(Thread runnable) throws InterruptedException;
    }
}
