/**
 * 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.openejb.junit5.jee;

import org.apache.openejb.Injector;
import org.apache.openejb.OpenEJBException;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.OpenEjbContainer;
import org.apache.openejb.injection.FallbackPropertyInjector;
import org.apache.openejb.junit.jee.config.Properties;
import org.apache.openejb.junit.jee.config.Property;
import org.apache.openejb.junit.jee.config.PropertyFile;
import org.apache.openejb.junit.jee.resources.TestResource;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.osgi.client.LocalInitialContextFactory;
import org.apache.openejb.testing.TestInstance;
import org.apache.openejb.util.Classes;
import org.junit.jupiter.api.extension.*;

import jakarta.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Hashtable;
import java.util.Optional;

public class EjbContainerExtension implements AfterAllCallback, BeforeAllCallback, BeforeEachCallback {

    private java.util.Properties properties;
    private EJBContainer container;

    @Override
    public void beforeAll(ExtensionContext extensionContext) throws Exception {

        Class<?> clazz = extensionContext.getTestClass()
                .orElseThrow(() -> new OpenEJBRuntimeException("Could not get class from extension context"));

        properties = new java.util.Properties();

        { // set caller first to let it be overridable by @Property
            final StringBuilder b = new StringBuilder();
            for (final Class<?> c : Classes.ancestors(clazz)) {
                if (c != Object.class) {
                    b.append(c.getName()).append(",");
                }
            }
            b.setLength(b.length() - 1);
            properties.put(OpenEjbContainer.Provider.OPENEJB_ADDITIONNAL_CALLERS_KEY, b.toString());
        }

        // default implicit config
        {
            try (final InputStream is = clazz.getClassLoader().getResourceAsStream("openejb-junit.properties")) {
                if (is != null) {
                    properties.load(is);
                }
            }
        }

        final PropertyFile propertyFile = clazz.getAnnotation(PropertyFile.class);
        if (propertyFile != null) {
            final String path = propertyFile.value();
            if (!path.isEmpty()) {
                InputStream is = null;
                try {
                    is = clazz.getClassLoader().getResourceAsStream(path);
                    if (is == null) {
                        final File file = new File(path);
                        if (file.exists()) {
                            is = new FileInputStream(file);
                        } else {
                            throw new OpenEJBException("properties resource '" + path + "' not found");
                        }
                    }

                    properties.load(is);
                } finally {
                    if (is != null) {
                        is.close();
                    }
                }

            }
        }

        final Properties annotationConfig = clazz.getAnnotation(Properties.class);
        if (annotationConfig != null) {
            for (final Property property : annotationConfig.value()) {
                properties.put(property.key(), property.value());
            }
        }

        if (!properties.containsKey(Context.INITIAL_CONTEXT_FACTORY)) {
            properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, LocalInitialContextFactory.class.getName());
        }

        container = EJBContainer.createEJBContainer(properties);
    }

    @Override
    public void beforeEach(ExtensionContext extensionContext) throws Exception {
        Class<?> clazz = extensionContext.getTestClass().orElse(null);
        Object test =  extensionContext.getTestInstance().orElse(null);

        if (clazz != null){

            while (!Object.class.equals(clazz)) {
                for (final Field field : clazz.getDeclaredFields()) {
                    final TestResource resource = field.getAnnotation(TestResource.class);
                    if (resource != null) {
                        if (Context.class.isAssignableFrom(field.getType())) {
                            field.setAccessible(true);
                            field.set(Modifier.isStatic(field.getModifiers()) ? null : test, getContainer().getContext());
                        } else if (Hashtable.class.isAssignableFrom(field.getType())) {
                            field.setAccessible(true);
                            field.set(Modifier.isStatic(field.getModifiers()) ? null : test, getProperties());
                        } else if (EJBContainer.class.isAssignableFrom(field.getType())) {
                            field.setAccessible(true);
                            field.set(Modifier.isStatic(field.getModifiers()) ? null : test, getContainer());
                        } else {
                            throw new OpenEJBException("can't inject field '" + field.getName() + "'");
                        }
                    }
                }
                clazz = clazz.getSuperclass();
            }
        }

        if (test != null) {
            SystemInstance.get().setComponent(TestInstance.class, new TestInstance(test.getClass(), test));
            SystemInstance.get().getComponent(FallbackPropertyInjector.class); // force eager init (MockitoInjector initialize everything in its constructor)
            Injector.inject(test);
        }
    }

    @Override
    public void afterAll(ExtensionContext extensionContext) throws Exception {
        if (container != null) {
            container.close();
        }
    }

    public java.util.Properties getProperties() {
        return properties;
    }

    public EJBContainer getContainer() {
        return container;
    }
}
