/*
 * 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.kafka.common.internals;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;

import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionException;

import javax.security.auth.Subject;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class SecurityManagerCompatibilityTest {

    @EnabledForJreRange(min = JRE.JAVA_8, max = JRE.JAVA_22)
    @Test
    public void testLegacyStrategyLoadable() throws ClassNotFoundException, NoSuchMethodException {
        new LegacyStrategy(ReflectiveStrategy.Loader.forName());
    }

    @EnabledForJreRange(min = JRE.JAVA_18)
    @Test
    public void testModernStrategyLoadable() throws ClassNotFoundException, NoSuchMethodException {
        new ModernStrategy(ReflectiveStrategy.Loader.forName());
    }

    @Test
    public void testCompositeStrategyLoadable() {
        new CompositeStrategy(ReflectiveStrategy.Loader.forName());
    }

    @Test
    public void testDefaultStrategyLoadable() {
        assertNotNull(SecurityManagerCompatibility.get());
    }

    @Test
    public void testDefaultStrategyDoPrivilegedReturn() {
        Object object = new Object();
        Object returned = SecurityManagerCompatibility.get().doPrivileged(() -> object);
        assertSame(object, returned);
    }

    @Test
    public void testDefaultStrategyDoPrivilegedThrow() {
        assertThrows(RuntimeException.class, () ->
                SecurityManagerCompatibility.get().doPrivileged(() -> {
                    throw new RuntimeException();
                })
        );
    }

    @Test
    public void testDefaultStrategyCurrentNull() {
        Subject current = SecurityManagerCompatibility.get().current();
        assertNull(current);
    }

    @Test
    public void testDefaultStrategyCallAsReturn() {
        Subject subject = new Subject();
        Object object = new Object();
        Object returned = SecurityManagerCompatibility.get().callAs(subject, () -> object);
        assertSame(object, returned);
    }

    @Test
    public void testDefaultStrategyCallAsCurrent() {
        Subject subject = new Subject();
        Subject returned = SecurityManagerCompatibility.get().callAs(subject, SecurityManagerCompatibility.get()::current);
        assertSame(subject, returned);
    }

    @Test
    public void testLegacyStrategyThrowsWhenSecurityManagerRemoved() {
        ReflectiveStrategy.Loader loader = simulateSecurityManagerRemoval();
        assertThrows(ClassNotFoundException.class, () -> new LegacyStrategy(loader));
    }

    @EnabledForJreRange(min = JRE.JAVA_18)
    @Test
    public void testModernStrategyLoadableWhenSecurityManagerRemoved() throws ClassNotFoundException, NoSuchMethodException {
        ReflectiveStrategy.Loader loader = simulateSecurityManagerRemoval();
        new ModernStrategy(loader);
    }

    @Test
    public void testCompositeStrategyLoadableWhenSecurityManagerRemoved() {
        ReflectiveStrategy.Loader loader = simulateSecurityManagerRemoval();
        new CompositeStrategy(loader);
    }

    @Test
    public void testLegacyStrategyCurrentThrowsWhenSecurityManagerUnsupported() throws ClassNotFoundException, NoSuchMethodException {
        ReflectiveStrategy.Loader loader = simulateMethodsThrowUnsupportedOperationExceptions();
        SecurityManagerCompatibility legacy = new LegacyStrategy(loader);
        assertThrows(UnsupportedOperationException.class, legacy::current);
    }

    @Test
    public void testLegacyStrategyCallAsThrowsWhenSecurityManagerUnsupported() throws ClassNotFoundException, NoSuchMethodException {
        ReflectiveStrategy.Loader loader = simulateMethodsThrowUnsupportedOperationExceptions();
        SecurityManagerCompatibility legacy = new LegacyStrategy(loader);
        assertThrows(UnsupportedOperationException.class, () -> legacy.callAs(null, () -> null));
    }

    @Test
    public void testCompositeStrategyDoPrivilegedWhenSecurityManagerUnsupported() {
        ReflectiveStrategy.Loader loader = simulateMethodsThrowUnsupportedOperationExceptions();
        CompositeStrategy composite = new CompositeStrategy(loader);
        Object object = new Object();
        Object returned = composite.doPrivileged(() -> object);
        assertSame(object, returned);
    }

    @Test
    public void testCompositeStrategyCurrentWhenSecurityManagerUnsupported() {
        ReflectiveStrategy.Loader loader = simulateMethodsThrowUnsupportedOperationExceptions();
        CompositeStrategy composite = new CompositeStrategy(loader);
        Object returned = composite.current();
        assertNull(returned);
    }

    @Test
    public void testCompositeStrategyCallAsWhenSecurityManagerUnsupported() {
        ReflectiveStrategy.Loader loader = simulateMethodsThrowUnsupportedOperationExceptions();
        CompositeStrategy composite = new CompositeStrategy(loader);
        Subject subject = new Subject();
        Subject returned = composite.callAs(subject, composite::current);
        assertSame(subject, returned);
    }

    private ReflectiveStrategy.Loader simulateSecurityManagerRemoval() {
        return name -> {
            if (name.equals("java.security.AccessController")) {
                throw new ClassNotFoundException();
            } else {
                return ReflectiveStrategy.Loader.forName().loadClass(name);
            }
        };
    }

    private ReflectiveStrategy.Loader simulateMethodsThrowUnsupportedOperationExceptions() {
        // WARNING: These assertions are here to prevent warnings about unused methods.
        // These methods are used reflectively, and can't be removed.
        assertThrows(UnsupportedOperationException.class, () -> UnsupportedOperations.doPrivileged(null));
        assertThrows(UnsupportedOperationException.class, () -> UnsupportedOperations.getSubject(null));
        assertThrows(UnsupportedOperationException.class, () -> UnsupportedOperations.doAs(null, null));
        assertNull(UnsupportedOperations.current());
        assertNull(UnsupportedOperations.callAs(null, () -> null));
        return name -> {
            switch (name) {
                case "java.security.AccessController":
                case "javax.security.auth.Subject":
                    return UnsupportedOperations.class;
                case "java.security.AccessControlContext":
                    return UnsupportedOperations.DummyContext.class;
                default:
                    return ReflectiveStrategy.Loader.forName().loadClass(name);
            }
        };
    }

    /**
     * This is a class meant to stand-in for the AccessController, and Subject classes.
     * It simulates a scenario where all legacy methods throw UnsupportedOperationException, and only the modern
     * methods are functional.
     */
    public static class UnsupportedOperations {

        /**
         * This class stands-in for the AccessControlContext in the mocked signatures below, because we can't have a
         * compile-time dependency on the real class. This needs no methods and is just a dummy class.
         */
        public static class DummyContext {
        }

        private static final ThreadLocal<Subject> ACTIVE_SUBJECT = new ThreadLocal<>();

        /**
         * Copy of AccessController#doPrivileged
         */
        public static <T> void doPrivileged(PrivilegedAction<T> ignored) {
            throw new UnsupportedOperationException();
        }

        /**
         * Copy of AccessController#getContext
         */
        public static DummyContext getContext() {
            throw new UnsupportedOperationException();
        }

        /**
         * Copy of Subject#getSubject
         */
        public static void getSubject(DummyContext ignored) {
            throw new UnsupportedOperationException();
        }

        /**
         * Copy of Subject#doAs
         */
        public static <T> void doAs(Subject ignored1, PrivilegedExceptionAction<T> ignored2) {
            throw new UnsupportedOperationException();
        }

        /**
         * Copy of Subject#current
         */
        public static Subject current() {
            return ACTIVE_SUBJECT.get();
        }

        /**
         * Copy of Subject#callAs
         */
        public static <T> T callAs(Subject subject, Callable<T> action) throws CompletionException {
            Subject previous = ACTIVE_SUBJECT.get();
            ACTIVE_SUBJECT.set(subject);
            try {
                return action.call();
            } catch (Throwable e) {
                throw new CompletionException(e);
            } finally {
                ACTIVE_SUBJECT.set(previous);
            }
        }
    }
}
