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

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionPattern;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBits;
import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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.mockito.Mockito.when;

public class PermissionEntryCacheTest {

    private PermissionEntryCache cache = new PermissionEntryCache();

    private PermissionEntry permissionEntry;
    private PrincipalPermissionEntries ppe;
    private PermissionStore store;

    @Before
    public void before() {
        permissionEntry = new PermissionEntry("/path", true, 0, PrivilegeBits.BUILT_IN.get(PrivilegeBits.JCR_READ), RestrictionPattern.EMPTY);
        ppe = new PrincipalPermissionEntries();
        ppe.putEntriesByPath("/path", Set.of(permissionEntry));

        store = Mockito.mock(PermissionStore.class);

    }

    @Test
    public void testMissingInit() throws Exception {
        Map<String, PrincipalPermissionEntries> entries = inspectEntries(cache);
        assertNotNull(entries);
        assertTrue(entries.isEmpty());
    }

    @Test
    public void testInit() throws Exception {
        cache.init("a", 5);

        PrincipalPermissionEntries entries = inspectEntries(cache, "a");
        assertNotNull(entries);
        assertFalse(entries.isFullyLoaded());
        assertEquals(0, entries.getSize());
    }

    @Test
    public void testInitTwice() throws Exception {
        cache.init("a", 5);
        cache.init("a", 25);

        PrincipalPermissionEntries entries = inspectEntries(cache, "a");
        assertNotNull(entries);

        Field f = PrincipalPermissionEntries.class.getDeclaredField("expectedSize");
        f.setAccessible(true);

        long expectedSize = (long) f.get(entries);
        assertEquals(5, expectedSize);
        assertFalse(entries.isFullyLoaded());
    }

    @Test
    public void testInitDifferentPrincipal() throws Exception {
        cache.init("a", 5);

        PrincipalPermissionEntries entries = inspectEntries(cache, "notInitialized");
        assertNull(entries);
    }

    @Test
    public void testLoadMissingInit() {
        ppe.setFullyLoaded(true);
        when(store.load("a")).thenReturn(ppe);

        Collection<PermissionEntry> result = new TreeSet<>();
        cache.load(store, result, "a", "/path");

        assertTrue(result.isEmpty());
    }

    @Test
    public void testLoadNotComplete() throws Exception {
        cache.init("a", Long.MAX_VALUE);

        Collection<PermissionEntry> entries = Set.of(permissionEntry);
        when(store.load("a", "/path")).thenReturn(entries);

        Collection<PermissionEntry> result = new HashSet<>();
        cache.load(store, result, "a", "/path");

        assertEquals(entries, result);

        PrincipalPermissionEntries inspectedEntries = inspectEntries(cache, "a");
        assertFalse(inspectedEntries.isFullyLoaded());

        // requesting the entries again must NOT hit the store
        when(store.load("a", "/path")).thenThrow(IllegalStateException.class);

        result.clear();
        cache.load(store, result, "a", "/path");

        assertEquals(entries, result);
    }

    @Test
    public void testLoadCompleted() throws Exception {
        cache.init("a", 1);

        Collection<PermissionEntry> entries = Set.of(permissionEntry);
        when(store.load("a", "/path")).thenReturn(entries);

        Collection<PermissionEntry> result = new HashSet<>();
        cache.load(store, result, "a", "/path");

        assertEquals(entries, result);

        PrincipalPermissionEntries inspectedEntries = inspectEntries(cache, "a");
        assertTrue(inspectedEntries.isFullyLoaded());

        // requesting the entries again must NOT hit the store
        when(store.load("a", "/path")).thenThrow(IllegalStateException.class);

        result.clear();
        cache.load(store, result, "a", "/path");

        assertEquals(entries, result);
    }

    @Test
    public void testLoadNonExistingNotComplete() throws Exception {
        cache.init("a", Long.MAX_VALUE);

        when(store.load("a", "/path")).thenReturn(null);

        Collection<PermissionEntry> result = new HashSet<>();
        cache.load(store, result, "a", "/path");

        assertTrue(result.isEmpty());

        PrincipalPermissionEntries inspectedEntries = inspectEntries(cache, "a");
        assertFalse(inspectedEntries.isFullyLoaded());

        // requesting the entries again must NOT hit the store
        when(store.load("a", "/path")).thenThrow(IllegalStateException.class);

        result.clear();
        cache.load(store, result, "a", "/path");

        assertTrue(result.isEmpty());
    }

    @Test
    public void testLoadNonExistingNotCompleted2() throws Exception {
        cache.init("a", 1);
        when(store.load("a", "/path")).thenReturn(null);

        Collection<PermissionEntry> result = new HashSet<>();
        cache.load(store, result, "a", "/path");

        assertTrue(result.isEmpty());

        PrincipalPermissionEntries inspectedEntries = inspectEntries(cache, "a");
        assertFalse(inspectedEntries.isFullyLoaded());

        // requesting the entries again must NOT hit the store
        when(store.load("a", "/path")).thenThrow(IllegalStateException.class);

        result.clear();
        cache.load(store, result, "a", "/path");

        assertTrue(result.isEmpty());
    }

    @Test
    public void testLoadNonExistingCompleted() throws Exception {
        cache.init("a", 0);
        when(store.load("a", "/path")).thenReturn(null);

        Collection<PermissionEntry> result = new HashSet<>();
        cache.load(store, result, "a", "/path");

        assertTrue(result.isEmpty());

        PrincipalPermissionEntries inspectedEntries = inspectEntries(cache, "a");
        assertTrue(inspectedEntries.isFullyLoaded());

        // requesting the entries again must NOT hit the store
        when(store.load("a", "/path")).thenThrow(IllegalStateException.class);

        result.clear();
        cache.load(store, result, "a", "/path");

        assertTrue(result.isEmpty());
    }


    @Test
    public void testGetFullyLoadedEntries() throws Exception {
        ppe.setFullyLoaded(true);
        when(store.load("a")).thenReturn(ppe);

        PrincipalPermissionEntries entries = cache.getFullyLoadedEntries(store, "a");
        assertSame(ppe, entries);

        PrincipalPermissionEntries inspectedEntries = inspectEntries(cache, "a");
        assertSame(ppe, inspectedEntries);

        // requesting the entries again must NOT hit the store
        when(store.load("a")).thenThrow(IllegalStateException.class);
        entries = cache.getFullyLoadedEntries(store, "a");
        assertSame(ppe, entries);
    }

    private static PrincipalPermissionEntries inspectEntries(@NotNull PermissionEntryCache cache, @NotNull String principalName) throws Exception {
        Map<String, PrincipalPermissionEntries> entries = inspectEntries(cache);
        return entries.get(principalName);
    }

    private static Map<String, PrincipalPermissionEntries> inspectEntries(@NotNull PermissionEntryCache cache) throws Exception {
        Field f = PermissionEntryCache.class.getDeclaredField("entries");
        f.setAccessible(true);

        return (Map<String, PrincipalPermissionEntries>) f.get(cache);
    }
}
