/*
 *  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.isis.core.integtestsupport;

import java.util.List;
import java.util.Set;

import com.google.common.base.Joiner;
import com.google.common.collect.Lists;

import org.junit.Before;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import org.apache.isis.applib.AppManifest;
import org.apache.isis.applib.DomainObjectContainer;
import org.apache.isis.applib.fixtures.FixtureClock;
import org.apache.isis.applib.fixtures.InstallableFixture;
import org.apache.isis.applib.services.command.Command;
import org.apache.isis.applib.services.command.CommandContext;
import org.apache.isis.core.commons.authentication.AuthenticationSession;
import org.apache.isis.core.commons.config.IsisConfiguration;
import org.apache.isis.core.commons.config.IsisConfigurationDefault;
import org.apache.isis.core.metamodel.deployment.DeploymentCategory;
import org.apache.isis.core.metamodel.services.ServicesInjector;
import org.apache.isis.core.metamodel.specloader.validator.MetaModelInvalidException;
import org.apache.isis.core.runtime.authentication.AuthenticationManager;
import org.apache.isis.core.runtime.authentication.AuthenticationRequest;
import org.apache.isis.core.runtime.fixtures.FixturesInstallerDelegate;
import org.apache.isis.core.runtime.headless.IsisComponentProviderDefault;
import org.apache.isis.core.runtime.headless.IsisSystem;
import org.apache.isis.core.runtime.logging.IsisLoggingConfigurer;
import org.apache.isis.core.runtime.system.context.IsisContext;
import org.apache.isis.core.runtime.system.persistence.PersistenceSession;
import org.apache.isis.core.runtime.system.session.IsisSessionFactory;
import org.apache.isis.core.runtime.system.session.IsisSessionFactoryBuilder;
import org.apache.isis.core.runtime.system.transaction.IsisTransaction;
import org.apache.isis.core.runtime.system.transaction.IsisTransaction.State;
import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager;
import org.apache.isis.core.runtime.systemusinginstallers.IsisComponentProvider;
import org.apache.isis.core.security.authentication.AuthenticationRequestNameOnly;
import org.apache.isis.core.specsupport.scenarios.DomainServiceProvider;

import static org.junit.Assert.fail;

/**
 * Wraps a plain {@link IsisSessionFactoryBuilder}, and provides a number of features to assist with testing.
 *
 * @deprecated - to be replaced by {@link IsisSystem}.
 */
@Deprecated
public class IsisSystemForTest implements org.junit.rules.TestRule, DomainServiceProvider {

    //region > Listener, ListenerAdapter
    public interface Listener {

        void init(IsisConfiguration configuration) throws Exception;
        
        void preOpenSession(boolean firstTime) throws Exception;
        void postOpenSession(boolean firstTime) throws Exception;
        
        void preNextSession() throws Exception;
        void postNextSession() throws Exception;

        void preCloseSession() throws Exception;
        void postCloseSession() throws Exception;
    }
    
    public static abstract class ListenerAdapter implements Listener {
        
        private IsisConfiguration configuration;

        public void init(IsisConfiguration configuration) throws Exception {
            this.configuration = configuration;
        }
        
        protected IsisConfiguration getConfiguration() {
            return configuration;
        }

        @Override
        public void preOpenSession(boolean firstTime) throws Exception {
        }

        @Override
        public void postOpenSession(boolean firstTime) throws Exception {
        }

        @Override
        public void preNextSession() throws Exception {
        }

        @Override
        public void postNextSession() throws Exception {
        }

        @Override
        public void preCloseSession() throws Exception {
        }

        @Override
        public void postCloseSession() throws Exception {
        }
    }

    //endregion

    //region > getElseNull, get, set

    private static ThreadLocal<IsisSystemForTest> ISFT = new ThreadLocal<>();

    public static IsisSystemForTest getElseNull() {
        return ISFT.get();
    }
    
    public static IsisSystemForTest get() {
        final IsisSystemForTest isft = ISFT.get();
        if(isft == null) {
            throw new IllegalStateException("No IsisSystemForTest available on thread; call #set(IsisSystemForTest) first");
        }

        return isft;
    }

    public static void set(IsisSystemForTest isft) {
        ISFT.set(isft);
    }
    //endregion

    //region > Builder


    public static class Builder {

        private AuthenticationRequest authenticationRequest = new AuthenticationRequestNameOnly("tester");

        private IsisConfigurationDefault configuration = new IsisConfigurationDefault();

        private AppManifest appManifestIfAny;

        private final List<Object> services = Lists.newArrayList();
        private final List<InstallableFixture> fixtures = Lists.newArrayList();

        private final List <Listener> listeners = Lists.newArrayList();

        private org.apache.log4j.Level level;

        public Builder with(IsisConfiguration configuration) {
            this.configuration = (IsisConfigurationDefault) configuration;
            return this;
        }

        public Builder with(AuthenticationRequest authenticationRequest) {
            this.authenticationRequest = authenticationRequest;
            return this;
        }

        public Builder with(AppManifest appManifest) {
            this.appManifestIfAny = appManifest;
            return this;
        }

        public Builder withLoggingAt(org.apache.log4j.Level level) {
            this.level = level;
            return this;
        }

        public IsisSystemForTest build() {
            final IsisSystemForTest isisSystemForTest =
                    new IsisSystemForTest(
                            appManifestIfAny,
                            configuration,
                            authenticationRequest,
                            listeners);
            if(level != null) {
                isisSystemForTest.setLevel(level);
            }

            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public synchronized void run() {
                    try {
                        isisSystemForTest.closeSession();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    try {
                        if(isisSystemForTest.isisSessionFactory != null) {
                            isisSystemForTest.isisSessionFactory.destroyServicesAndShutdown();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });

            return isisSystemForTest;
        }


        public Builder with(Listener listener) {
            if(listener != null) {
                listeners.add(listener);
            }
            return this;
        }

    }

    public static Builder builder() {
        return new Builder();
    }

    //endregion

    //region > constructor, fields

    // these fields 'xxxForComponentProvider' are used to initialize the IsisComponentProvider, but shouldn't be used thereafter.
    private final AppManifest appManifestIfAny;
    private final IsisConfiguration configurationOverride;

    private final AuthenticationRequest authenticationRequestIfAny;
    private AuthenticationSession authenticationSession;


    private IsisSystemForTest(
            final AppManifest appManifestIfAny,
            final IsisConfiguration configurationOverride,
            final AuthenticationRequest authenticationRequestIfAny,
            final List<Listener> listeners) {
        this.appManifestIfAny = appManifestIfAny;
        this.configurationOverride = configurationOverride;
        this.authenticationRequestIfAny = authenticationRequestIfAny;
        this.listeners = listeners;
    }

    //endregion

    //region > level
    private org.apache.log4j.Level level = org.apache.log4j.Level.INFO;

    /**
     * The level to use for the root logger if fallback (ie a <tt>logging.properties</tt> file cannot be found).
     */
    public org.apache.log4j.Level getLevel() {
        return level;
    }
    
    public void setLevel(org.apache.log4j.Level level) {
        this.level = level;
    }

    //endregion

    //region > setup (also componentProvider)

    // populated at #setupSystem
    private IsisComponentProvider componentProvider;

    /**
     * Intended to be called from a test's {@link Before} method.
     */
    public IsisSystemForTest setUpSystem() throws RuntimeException {
        try {
            initIfRequiredThenOpenSession(FireListeners.FIRE);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return this;
    }

    private void initIfRequiredThenOpenSession(FireListeners fireListeners) throws Exception {

        // exit as quickly as possible for this case...
        final MetaModelInvalidException mmie = IsisContext.getMetaModelInvalidExceptionIfAny();
        if(mmie != null) {
            final Set<String> validationErrors = mmie.getValidationErrors();
            final String validationMsg = Joiner.on("\n").join(validationErrors);
            fail(validationMsg);
            return;
        }

        boolean firstTime = isisSessionFactory == null;
        if(fireListeners.shouldFire()) {
            fireInitAndPreOpenSession(firstTime);
        }

        if(firstTime) {
            IsisLoggingConfigurer isisLoggingConfigurer = new IsisLoggingConfigurer(getLevel());
            isisLoggingConfigurer.configureLogging(".", new String[] {});

            componentProvider = new IsisComponentProviderDefault(
                    appManifestIfAny,
                    configurationOverride
            );

            final IsisSessionFactoryBuilder isisSessionFactoryBuilder = new IsisSessionFactoryBuilder(componentProvider, DeploymentCategory.PRODUCTION, appManifestIfAny);

            // ensures that a FixtureClock is installed as the singleton underpinning the ClockService
            FixtureClock.initialize();

            isisSessionFactory = isisSessionFactoryBuilder.buildSessionFactory();
            // REVIEW: does no harm, but is this required?
            closeSession();

            // if the IsisSystem does not initialize properly, then - as a side effect - the resulting
            // MetaModelInvalidException will be pushed onto the IsisContext (as a static field).
            final MetaModelInvalidException ex = IsisContext.getMetaModelInvalidExceptionIfAny();
            if (ex != null) {

                // for subsequent tests; the attempt to bootstrap the framework will leave
                // the IsisContext singleton as set.
                IsisContext.testReset();

                final Set<String> validationErrors = ex.getValidationErrors();
                final StringBuilder buf = new StringBuilder();
                for (String validationError : validationErrors) {
                    buf.append(validationError).append("\n");
                }
                fail("Metamodel is invalid: \n" + buf.toString());
            }
        }

        final AuthenticationManager authenticationManager = isisSessionFactory.getAuthenticationManager();
        authenticationSession = authenticationManager.authenticate(authenticationRequestIfAny);

        openSession();

        if(fireListeners.shouldFire()) {
            firePostOpenSession(firstTime);
        }
    }

    public DomainObjectContainer getContainer() {
        return getService(DomainObjectContainer.class);
    }

    //endregion

    //region > isisSystem (populated during setup)
    private IsisSessionFactory isisSessionFactory;

    /**
     * The {@link IsisSessionFactory} created during {@link #setUpSystem()}.
     */
    public IsisSessionFactory getIsisSessionFactory() {
        return isisSessionFactory;
    }

    /**
     * The {@link AuthenticationSession} created during {@link #setUpSystem()}.
     */
    public AuthenticationSession getAuthenticationSession() {
        return authenticationSession;
    }

    //endregion

    //region > teardown

    private void closeSession(final FireListeners fireListeners) throws Exception {
        if(fireListeners.shouldFire()) {
            firePreCloseSession();
        }
        if(isisSessionFactory.inSession()) {
            isisSessionFactory.closeSession();
        }
        if(fireListeners.shouldFire()) {
            firePostCloseSession();
        }
    }

    public void nextSession() throws Exception {
        firePreNextSession();
        closeSession();
        openSession();
        firePostNextSession();
    }

    //endregion

    //region > openSession, closeSession
    public void openSession() throws Exception {
        openSession(authenticationSession);

    }

    public void openSession(AuthenticationSession authenticationSession) throws Exception {
        isisSessionFactory.openSession(authenticationSession);
    }

    public void closeSession() throws Exception {
        closeSession(FireListeners.FIRE);
    }

    //endregion

    //region > listeners

    private List <Listener> listeners;

    private enum FireListeners {
        FIRE,
        DONT_FIRE;
        public boolean shouldFire() {
            return this == FIRE;
        }
    }


    private void fireInitAndPreOpenSession(boolean firstTime) throws Exception {
        if(firstTime) {
            for(Listener listener: listeners) {
                listener.init(componentProvider.getConfiguration());
            }
        }
        for(Listener listener: listeners) {
            listener.preOpenSession(firstTime);
        }
    }

    private void firePostOpenSession(boolean firstTime) throws Exception {
        for(Listener listener: listeners) {
            listener.postOpenSession(firstTime);
        }
    }

    private void firePreCloseSession() throws Exception {
        for(Listener listener: listeners) {
            listener.preCloseSession();
        }
    }

    private void firePostCloseSession() throws Exception {
        for(Listener listener: listeners) {
            listener.postCloseSession();
        }
    }

    private void firePreNextSession() throws Exception {
        for(Listener listener: listeners) {
            listener.preNextSession();
        }
    }

    private void firePostNextSession() throws Exception {
        for(Listener listener: listeners) {
            listener.postNextSession();
        }
    }
    //endregion

    //region > JUnit @Rule integration

    @Override
    public Statement apply(final Statement base, final Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                setUpSystem();
                try {
                    base.evaluate();
                    closeSession();
                } catch(Throwable ex) {
                    try {
                        closeSession();
                    } catch(Exception ex2) {
                        // ignore, since already one pending
                    }
                    throw ex;
                }
            }
        };
    }

    //endregion

    //region > beginTran, endTran, commitTran, abortTran

    /**
     * @deprecated - ought to be using regular domain services rather than reaching into the framework
     */
    @Deprecated
    public void beginTran() {
        final IsisTransactionManager transactionManager = getTransactionManager();
        final IsisTransaction transaction = transactionManager.getCurrentTransaction();

        if(transaction == null) {
            startTransactionForUser(transactionManager);
            return;
        }

        final State state = transaction.getState();
        switch(state) {
            case COMMITTED:
            case ABORTED:
                startTransactionForUser(transactionManager);
                break;
            case IN_PROGRESS:
                // nothing to do
                break;
            case MUST_ABORT:
                fail("Transaction is in state of '" + state + "'");
                break;
            default:
                fail("Unknown transaction state '" + state + "'");
        }

    }

    private void startTransactionForUser(IsisTransactionManager transactionManager) {
        transactionManager.startTransaction();

        // specify that this command (if any) is being executed by a 'USER'
        final CommandContext commandContext = getService(CommandContext.class);
        Command command = commandContext.getCommand();
        command.setExecutor(Command.Executor.USER);
    }

    /**
     * Either commits or aborts the transaction, depending on the Transaction's {@link org.apache.isis.core.runtime.system.transaction.IsisTransaction#getState()}
     *
     * @deprecated - ought to be using regular domain services rather than reaching into the framework
     */
    @Deprecated
    public void endTran() {
        final IsisTransactionManager transactionManager = getTransactionManager();
        final IsisTransaction transaction = transactionManager.getCurrentTransaction();
        if(transaction == null) {
            fail("No transaction exists");
            return;
        }

        transactionManager.endTransaction();

        final State state = transaction.getState();
        switch(state) {
            case COMMITTED:
                break;
            case ABORTED:
                break;
            case IN_PROGRESS:
                fail("Transaction is still in state of '" + state + "'");
                break;
            case MUST_ABORT:
                fail("Transaction is still in state of '" + state + "'");
                break;
            default:
                fail("Unknown transaction state '" + state + "'");
        }
    }

    /**
     * Commits the transaction.
     *
     * @deprecated - ought to be using regular domain services rather than reaching into the framework
     */
    @Deprecated
    public void commitTran() {
        final IsisTransactionManager transactionManager = getTransactionManager();
        final IsisTransaction transaction = transactionManager.getCurrentTransaction();
        if(transaction == null) {
            fail("No transaction exists");
            return;
        }
        final State state = transaction.getState();
        switch(state) {
            case COMMITTED:
            case ABORTED:
            case MUST_ABORT:
                fail("Transaction is in state of '" + state + "'");
                break;
            case IN_PROGRESS:
                transactionManager.endTransaction();
                break;
            default:
                fail("Unknown transaction state '" + state + "'");
        }
    }

    /**
     * Aborts the transaction.
     *
     * @deprecated - ought to be using regular domain services rather than reaching into the framework
     */
    @Deprecated
    public void abortTran() {
        final IsisTransactionManager transactionManager = getTransactionManager();
        final IsisTransaction transaction = transactionManager.getCurrentTransaction();
        if(transaction == null) {
            fail("No transaction exists");
            return;
        }
        final State state = transaction.getState();
        switch(state) {
            case ABORTED:
                break;
            case COMMITTED:
                fail("Transaction is in state of '" + state + "'");
                break;
            case MUST_ABORT:
            case IN_PROGRESS:
                transactionManager.abortTransaction();
                break;
            default:
                fail("Unknown transaction state '" + state + "'");
        }
    }

    //endregion

    //region > getService, replaceService

    /* (non-Javadoc)
     * @see org.apache.isis.core.integtestsupport.ServiceProvider#getService(java.lang.Class)
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getService(Class<T> serviceClass) {
        final ServicesInjector servicesInjector = isisSessionFactory.getServicesInjector();
        return servicesInjector.lookupServiceElseFail(serviceClass);
    }

    @Override
    public <T> void replaceService(final T originalService, final T replacementService) {
        final ServicesInjector servicesInjector = isisSessionFactory.getServicesInjector();
        servicesInjector.replaceService(originalService, replacementService);
    }

    //endregion

    //region > Fixture management (for each test, rather than at bootstrap)

    /**
     * @deprecated - use {@link org.apache.isis.applib.fixturescripts.FixtureScripts} domain service instead.
     */
    @Deprecated
    public void installFixtures(final InstallableFixture... fixtures) {
        final FixturesInstallerDelegate fid = new FixturesInstallerDelegate(isisSessionFactory);
        for (final InstallableFixture fixture : fixtures) {
            fid.addFixture(fixture);
        }
        fid.installFixtures();

        // ensure that tests are performed in separate xactn to any fixture setup.
        final IsisTransactionManager transactionManager = getTransactionManager();
        final IsisTransaction transaction = transactionManager.getCurrentTransaction();
        final State transactionState = transaction.getState();
        if(transactionState.canCommit()) {
            commitTran();
            try {
                nextSession();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            beginTran();
        }
    }

    //endregion

    //region > Dependencies

    private IsisTransactionManager getTransactionManager() {
        return getPersistenceSession().getTransactionManager();
    }
    
    private PersistenceSession getPersistenceSession() {
        return isisSessionFactory.getCurrentSession().getPersistenceSession();
    }

    //endregion

}
