/*
 * 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.deltaspike.testcontrol.impl.jsf;

import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.apache.deltaspike.testcontrol.spi.ExternalContainer;
import org.apache.myfaces.test.mock.MockApplicationFactory;
import org.apache.myfaces.test.mock.MockExceptionHandlerFactory;
import org.apache.myfaces.test.mock.MockExternalContext;
import org.apache.myfaces.test.mock.MockFacesContext;
import org.apache.myfaces.test.mock.MockFacesContextFactory;
import org.apache.myfaces.test.mock.MockHttpServletRequest;
import org.apache.myfaces.test.mock.MockHttpServletResponse;
import org.apache.myfaces.test.mock.MockHttpSession;
import org.apache.myfaces.test.mock.MockPartialViewContextFactory;
import org.apache.myfaces.test.mock.MockRenderKit;
import org.apache.myfaces.test.mock.MockRenderKitFactory;
import org.apache.myfaces.test.mock.MockServletConfig;
import org.apache.myfaces.test.mock.MockServletContext;
import org.apache.myfaces.test.mock.lifecycle.MockLifecycleFactory;
import org.apache.myfaces.test.mock.visit.MockVisitContextFactory;

import jakarta.el.ExpressionFactory;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.FactoryFinder;
import jakarta.faces.application.Application;
import jakarta.faces.application.ApplicationFactory;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.context.ExceptionHandler;
import jakarta.faces.context.ExceptionHandlerFactory;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.FacesContextFactory;
import jakarta.faces.lifecycle.Lifecycle;
import jakarta.faces.lifecycle.LifecycleFactory;
import jakarta.faces.render.RenderKit;
import jakarta.faces.render.RenderKitFactory;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

//known restriction: faces-config.xml files are ignored
@ApplicationScoped
public class MockedJsf2TestContainer implements ExternalContainer
{
    protected MockServletConfig servletConfig;
    protected MockServletContext servletContext;

    protected Lifecycle lifecycle;
    protected RenderKit renderKit;
    protected Application application;

    protected FacesContext facesContext;
    protected MockHttpServletRequest request;
    protected MockHttpServletResponse response;
    protected MockHttpSession session;

    protected Map<String, String> containerConfig;

    public void boot()
    {
        initContainerConfig();
        initServletObjects();
        initJsf();
    }

    protected void initContainerConfig()
    {
        containerConfig = new HashMap<String, String>();
        for (Map.Entry<String, String> entry : ConfigResolver.getAllProperties().entrySet())
        {
            if (entry.getKey().startsWith("org.apache.myfaces.") || entry.getKey().startsWith("jakarta.faces.") ||
                    entry.getKey().startsWith("facelets."))
            {
                containerConfig.put(entry.getKey(), entry.getValue());
            }
        }
    }

    protected void initServletObjects()
    {
        this.servletContext = new MockServletContext();
        this.servletConfig = new MockServletConfig(this.servletContext);
        applyContainerConfig();
    }

    protected void applyContainerConfig()
    {
        //add the default values
        servletContext.addInitParameter("jakarta.faces.PROJECT_STAGE", "UnitTest");
        servletContext.addInitParameter("jakarta.faces.PARTIAL_STATE_SAVING", "true");
        servletContext.addInitParameter("jakarta.faces.FACELETS_REFRESH_PERIOD", "-1");

        servletContext.addInitParameter("org.apache.myfaces.INITIALIZE_ALWAYS_STANDALONE", "true");
        servletContext.addInitParameter("org.apache.myfaces.spi.InjectionProvider",
            "org.apache.myfaces.spi.impl.NoInjectionAnnotationInjectionProvider");
        servletContext.addInitParameter("org.apache.myfaces.config.annotation.LifecycleProvider",
            "org.apache.myfaces.config.annotation.NoInjectionAnnotationLifecycleProvider");
        servletConfig.addInitParameter("org.apache.myfaces.CHECKED_VIEWID_CACHE_ENABLED", "false");

        servletContext.addInitParameter(ExpressionFactory.class.getName(), "org.apache.el.ExpressionFactoryImpl");
        //add custom values (might replace the default values)
        for (Map.Entry<String, String> entry : containerConfig.entrySet())
        {
            servletContext.addInitParameter(entry.getKey(), entry.getValue());
        }
    }

    protected void initJsf()
    {
        FactoryFinder.releaseFactories();

        onPreInitJsf();

        initLifecycle();
        initApplication();
        initRenderKit();
    }

    protected void onPreInitJsf()
    {
        //init mocked jsf factories
        addFactory(FactoryFinder.APPLICATION_FACTORY, MockApplicationFactory.class.getName());
        addFactory(FactoryFinder.FACES_CONTEXT_FACTORY, MockFacesContextFactory.class.getName());
        addFactory(FactoryFinder.LIFECYCLE_FACTORY, MockLifecycleFactory.class.getName());
        addFactory(FactoryFinder.RENDER_KIT_FACTORY, MockRenderKitFactory.class.getName());
        addFactory(FactoryFinder.EXCEPTION_HANDLER_FACTORY, MockExceptionHandlerFactory.class.getName());
        addFactory(FactoryFinder.PARTIAL_VIEW_CONTEXT_FACTORY, MockPartialViewContextFactory.class.getName());
        addFactory(FactoryFinder.VISIT_CONTEXT_FACTORY, MockVisitContextFactory.class.getName());
    }

    protected void addFactory(String factoryName, String className)
    {
        FactoryFinder.setFactory(factoryName, className);
    }

    protected void initLifecycle()
    {
        LifecycleFactory lifecycleFactory =
                (LifecycleFactory) FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
        this.lifecycle = lifecycleFactory.getLifecycle(getLifecycleId());
    }

    protected void initApplication()
    {
        ApplicationFactory applicationFactory =
                (ApplicationFactory) FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY);
        this.application = applicationFactory.getApplication();
    }

    protected void initRenderKit()
    {
        RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder
                .getFactory(FactoryFinder.RENDER_KIT_FACTORY);
        this.renderKit = new MockRenderKit();
        renderKitFactory.addRenderKit(RenderKitFactory.HTML_BASIC_RENDER_KIT, this.renderKit);
    }

    @Override
    public void startScope(Class<? extends Annotation> scopeClass)
    {
        if (RequestScoped.class.equals(scopeClass))
        {
            initRequest();
            initResponse();

            initFacesContext();

            initDefaultView();
        }
        else if (SessionScoped.class.equals(scopeClass))
        {
            initSession();
        }
    }

    protected void initRequest()
    {
        this.request = new MockHttpServletRequest(this.session);
        this.request.setServletContext(this.servletContext);
    }

    protected void initResponse()
    {
        this.response = new MockHttpServletResponse();
    }

    protected void initFacesContext()
    {
        FacesContextFactory facesContextFactory =
                (FacesContextFactory) FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
        this.facesContext = facesContextFactory.getFacesContext(
                this.servletContext, this.request, this.response, this.lifecycle);

        ((MockFacesContext) this.facesContext).setApplication(this.application);
        ExceptionHandler exceptionHandler = ((ExceptionHandlerFactory)
                FactoryFinder.getFactory(FactoryFinder.EXCEPTION_HANDLER_FACTORY)).getExceptionHandler();
        this.facesContext.setExceptionHandler(exceptionHandler);

        ((MockFacesContext) this.facesContext).setExternalContext(
                new MockExternalContext(this.servletContext, this.request, this.response));
    }

    protected void initDefaultView()
    {
        UIViewRoot root = new UIViewRoot();
        root.setViewId("/viewId");
        root.setLocale(getLocale());
        root.setRenderKitId(RenderKitFactory.HTML_BASIC_RENDER_KIT);
        this.facesContext.setViewRoot(root);
    }

    protected void initSession()
    {
        if (this.request != null)
        {
            this.session = (MockHttpSession)this.request.getSession(true);
        }
        else
        {
            this.session = new MockHttpSession();
            this.session.setServletContext(this.servletContext);
        }
    }

    @Override
    public void stopScope(Class<? extends Annotation> scopeClass)
    {
        if (RequestScoped.class.equals(scopeClass))
        {
            if (this.facesContext != null)
            {
                this.facesContext.release();
            }
            this.facesContext = null;
            this.request = null;
            this.response = null;
        }
        else if (SessionScoped.class.equals(scopeClass))
        {
            this.session = null;
        }
    }

    protected Locale getLocale()
    {
        return Locale.getDefault();
    }

    public void shutdown()
    {
        this.application = null;
        this.servletConfig = null;
        this.containerConfig = null;
        this.lifecycle = null;
        this.renderKit = null;
        this.servletContext = null;

        FactoryFinder.releaseFactories();
    }

    @Override
    public int getOrdinal()
    {
        return 1000; //default in ds
    }

    protected String getLifecycleId()
    {
        return LifecycleFactory.DEFAULT_LIFECYCLE;
    }
}
