/*
 * Copyright 2004-2013 the Seasar Foundation and the Others.
 *
 * Licensed 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.seasar.framework.unit;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.Servlet;

import junit.framework.TestCase;

import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.ExternalContext;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.deployer.ComponentDeployerFactory;
import org.seasar.framework.container.deployer.ExternalComponentDeployerProvider;
import org.seasar.framework.container.deployer.InstanceDefFactory;
import org.seasar.framework.container.external.servlet.HttpServletExternalContext;
import org.seasar.framework.container.external.servlet.HttpServletExternalContextComponentDefRegister;
import org.seasar.framework.container.factory.AnnotationHandler;
import org.seasar.framework.container.factory.AnnotationHandlerFactory;
import org.seasar.framework.container.factory.S2ContainerFactory;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;
import org.seasar.framework.container.impl.S2ContainerBehavior;
import org.seasar.framework.container.servlet.S2ContainerServlet;
import org.seasar.framework.convention.NamingConvention;
import org.seasar.framework.convention.impl.NamingConventionImpl;
import org.seasar.framework.env.Env;
import org.seasar.framework.exception.NoSuchMethodRuntimeException;
import org.seasar.framework.message.MessageResourceBundleFactory;
import org.seasar.framework.mock.servlet.MockHttpServletRequest;
import org.seasar.framework.mock.servlet.MockHttpServletResponse;
import org.seasar.framework.mock.servlet.MockHttpServletResponseImpl;
import org.seasar.framework.mock.servlet.MockServletConfig;
import org.seasar.framework.mock.servlet.MockServletConfigImpl;
import org.seasar.framework.mock.servlet.MockServletContext;
import org.seasar.framework.mock.servlet.MockServletContextImpl;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.DisposableUtil;
import org.seasar.framework.util.FieldUtil;
import org.seasar.framework.util.MethodUtil;
import org.seasar.framework.util.ResourceUtil;
import org.seasar.framework.util.StringUtil;

/**
 * Seasar2を使うテストを行なうための<code>TestCase</code>です。
 * 
 * @author higa
 */
public abstract class S2FrameworkTestCase extends TestCase {

    /**
     * 環境が設定されているファイルのパスです。
     */
    protected static final String ENV_PATH = "env_ut.txt";

    /**
     * 環境が設定されていない場合のデフォルト値です。
     */
    protected static final String ENV_VALUE = "ut";

    private S2Container container;

    private Servlet servlet;

    private MockServletConfig servletConfig;

    private MockServletContext servletContext;

    private MockHttpServletRequest request;

    private MockHttpServletResponse response;

    private ClassLoader originalClassLoader;

    private UnitClassLoader unitClassLoader;

    private NamingConvention namingConvention;

    private List boundFields;

    private boolean warmDeploy = true;

    private boolean registerNamingConvention = true;

    private AnnotationHandler annotationHandler = AnnotationHandlerFactory
            .getAnnotationHandler();

    /**
     * {@link S2FrameworkTestCase}を作成します。
     */
    public S2FrameworkTestCase() {
    }

    /**
     * {@link S2FrameworkTestCase}を作成します。
     * 
     * @param name
     */
    public S2FrameworkTestCase(String name) {
        super(name);
    }

    /**
     * WARM deployかどうかを返します。
     * 
     * @return WARM deployかどうか
     */
    public boolean isWarmDeploy() {
        return warmDeploy && !ResourceUtil.isExist("s2container.dicon")
                && ResourceUtil.isExist("convention.dicon")
                && ResourceUtil.isExist("creator.dicon")
                && ResourceUtil.isExist("customizer.dicon");
    }

    /**
     * WARM deployかどうかを設定します。
     * 
     * @param warmDeploy
     */
    public void setWarmDeploy(boolean warmDeploy) {
        this.warmDeploy = warmDeploy;
    }

    /**
     * テスト用のS2コンテナを作成する際に{@link NamingConvention}を登録する場合は<code>true</code>を返します。
     * 
     * @return テスト用のS2コンテナを作成する際に{@link NamingConvention}を登録する場合は<code>true</code>
     */
    public boolean isRegisterNamingConvention() {
        return registerNamingConvention;
    }

    /**
     * テスト用のS2コンテナを作成する際に{@link NamingConvention}を登録する場合は<code>true</code>を設定します。
     * 
     * @param registerNamingConvention
     *            テスト用のS2コンテナを作成する際に{@link NamingConvention}を登録する場合は<code>true</code>
     */
    public void setRegisterNamingConvention(boolean registerNamingConvention) {
        this.registerNamingConvention = registerNamingConvention;
    }

    /**
     * {@link S2Container}を返します。
     * 
     * @return {@link S2Container}
     */
    public S2Container getContainer() {
        return container;
    }

    /**
     * コンポーネントを返します。
     * 
     * @param componentName
     * @return
     */
    public Object getComponent(String componentName) {
        return container.getComponent(componentName);
    }

    /**
     * コンポーネントを返します。
     * 
     * @param componentClass
     * @return
     */
    public Object getComponent(Class componentClass) {
        return container.getComponent(componentClass);
    }

    /**
     * {@link ComponentDef}を返します。
     * 
     * @param componentName
     * @return {@link ComponentDef}
     */
    public ComponentDef getComponentDef(String componentName) {
        return container.getComponentDef(componentName);
    }

    /**
     * {@link ComponentDef}を返します。
     * 
     * @param componentClass
     * @return {@link ComponentDef}
     */
    public ComponentDef getComponentDef(Class componentClass) {
        return container.getComponentDef(componentClass);
    }

    /**
     * コンポーネントを登録します。
     * 
     * @param componentClass
     * @see #register(Class, String)
     */
    public void register(Class componentClass) {
        if (namingConvention == null) {
            register(componentClass, null);
        } else {
            register(componentClass, namingConvention
                    .fromClassNameToComponentName(componentClass.getName()));
        }
    }

    /**
     * コンポーネントを登録します。
     * 
     * @param componentClass
     * @param componentName
     */
    public void register(Class componentClass, String componentName) {
        ComponentDef cd = annotationHandler.createComponentDef(componentClass,
                InstanceDefFactory.SINGLETON);
        cd.setComponentName(componentName);
        annotationHandler.appendDI(cd);
        annotationHandler.appendAspect(cd);
        annotationHandler.appendInterType(cd);
        annotationHandler.appendInitMethod(cd);
        annotationHandler.appendDestroyMethod(cd);
        container.register(cd);
    }

    /**
     * コンポーネントを登録します。
     * 
     * @param component
     */
    public void register(Object component) {
        container.register(component);
    }

    /**
     * コンポーネントを登録します。
     * 
     * @param component
     * @param componentName
     */
    public void register(Object component, String componentName) {
        container.register(component, componentName);
    }

    /**
     * コンポーネントを登録します。
     * 
     * @param componentDef
     */
    public void register(ComponentDef componentDef) {
        container.register(componentDef);
    }

    /**
     * diconファイルをインクルードします。
     * 
     * @param path
     */
    public void include(String path) {
        S2ContainerFactory.include(container, convertPath(path));
    }

    /**
     * パスを変換します。
     * 
     * @param path
     *            パス
     * @return 変換後のパス
     */
    protected String convertPath(String path) {
        return ResourceUtil.convertPath(path, getClass());
    }

    /**
     * @see junit.framework.TestCase#runBare()
     */
    public void runBare() throws Throwable {
        setUpContainer();
        try {
            setUp();
            try {
                setUpForEachTestMethod();
                try {
                    container.init();
                    try {
                        setUpAfterContainerInit();
                        try {
                            bindFields();
                            try {
                                setUpAfterBindFields();
                                try {
                                    doRunTest();
                                } finally {
                                    tearDownBeforeUnbindFields();
                                }
                            } finally {
                                unbindFields();
                            }
                        } finally {
                            tearDownBeforeContainerDestroy();
                        }
                    } finally {
                        container.destroy();
                    }
                } finally {
                    tearDownForEachTestMethod();
                }
            } finally {
                tearDown();
            }
        } finally {
            tearDownContainer();
        }
    }

    /**
     * ルートのdiconのパスを返します。
     * 
     * @return ルートのdiconのパス
     * @throws Throwable
     */
    protected String getRootDicon() throws Throwable {
        return null;
    }

    /**
     * S2コンテナをセットアップします。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void setUpContainer() throws Throwable {
        Env.setFilePath(ENV_PATH);
        Env.setValueIfAbsent(ENV_VALUE);
        originalClassLoader = getOriginalClassLoader();
        unitClassLoader = new UnitClassLoader(originalClassLoader);
        Thread.currentThread().setContextClassLoader(unitClassLoader);
        if (isWarmDeploy()) {
            S2ContainerFactory.configure("warmdeploy.dicon");
        }
        String rootDicon = resolveRootDicon();
        container = StringUtil.isEmpty(rootDicon) ? S2ContainerFactory.create()
                : S2ContainerFactory.create(rootDicon);
        SingletonS2ContainerFactory.setContainer(container);
        if (servletContext == null) {
            servletContext = new MockServletContextImpl("s2-example");
        }
        request = servletContext.createRequest("/hello.html");
        response = new MockHttpServletResponseImpl(request);
        servletConfig = new MockServletConfigImpl();
        servletConfig.setServletContext(servletContext);
        servlet = new S2ContainerServlet();
        servlet.init(servletConfig);
        ExternalContext externalContext = new HttpServletExternalContext();
        externalContext.setApplication(servletContext);
        externalContext.setRequest(request);
        externalContext.setResponse(response);
        container.setExternalContext(externalContext);
        container
                .setExternalContextComponentDefRegister(new HttpServletExternalContextComponentDefRegister());
        ComponentDeployerFactory
                .setProvider(new ExternalComponentDeployerProvider());
        if (!container.hasComponentDef(NamingConvention.class)
                && isRegisterNamingConvention()) {
            namingConvention = new NamingConventionImpl();
            container.register(namingConvention);
        }
    }

    /**
     * オリジナルのクラスローダを返します。
     * 
     * @return オリジナルのクラスローダ
     */
    protected ClassLoader getOriginalClassLoader() {
        S2Container configurationContainer = S2ContainerFactory
                .getConfigurationContainer();
        if (configurationContainer != null
                && configurationContainer.hasComponentDef(ClassLoader.class)) {
            return (ClassLoader) configurationContainer
                    .getComponent(ClassLoader.class);
        }
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * ルートのdiconを解決します。
     * 
     * @return ルートのdicon
     * @throws Throwable
     *             例外が発生した場合
     */
    protected String resolveRootDicon() throws Throwable {
        String targetName = getTargetName();
        if (targetName.length() > 0) {
            try {
                Method method = getClass().getMethod(
                        "getRootDicon" + targetName, null);
                return (String) method.invoke(this, null);
            } catch (Exception ignore) {
            }
        }
        return getRootDicon();
    }

    /**
     * S2コンテナの終了処理を行ないます。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void tearDownContainer() throws Throwable {
        ComponentDeployerFactory
                .setProvider(new ComponentDeployerFactory.DefaultProvider());
        SingletonS2ContainerFactory.setContainer(null);
        S2ContainerServlet.clearInstance();
        MessageResourceBundleFactory.clear();
        DisposableUtil.dispose();
        S2ContainerBehavior
                .setProvider(new S2ContainerBehavior.DefaultProvider());
        Thread.currentThread().setContextClassLoader(originalClassLoader);
        unitClassLoader = null;
        originalClassLoader = null;
        container = null;
        servletContext = null;
        request = null;
        response = null;
        servletConfig = null;
        servlet = null;
        namingConvention = null;
        Env.initialize();
    }

    /**
     * S2コンテナが初期化された後のセットアップを行ないます。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void setUpAfterContainerInit() throws Throwable {
    }

    /**
     * フィールドのバインディング後のセットアップを行ないます。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void setUpAfterBindFields() throws Throwable {
    }

    /**
     * フィールドがアンバインディングされる前に終了処理を行ないます。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void tearDownBeforeUnbindFields() throws Throwable {
    }

    /**
     * テストメソッドごとのセットアップを行ないます。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void setUpForEachTestMethod() throws Throwable {
        String targetName = getTargetName();
        if (targetName.length() > 0) {
            invoke("setUp" + targetName);
        }
    }

    /**
     * S2コンテナが破棄される前に終了処理を行ないます。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void tearDownBeforeContainerDestroy() throws Throwable {
    }

    /**
     * テストメソッドごとの終了処理を行ないます。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void tearDownForEachTestMethod() throws Throwable {
        String targetName = getTargetName();
        if (targetName.length() > 0) {
            invoke("tearDown" + getTargetName());
        }
    }

    /**
     * テストを実行します。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void doRunTest() throws Throwable {
        runTest();
    }

    /**
     * サーブレットを返します。
     * 
     * @return サーブレット
     */
    protected Servlet getServlet() {
        return servlet;
    }

    /**
     * サーブレットを設定します。
     * 
     * @param servlet
     *            サーブレット
     */
    protected void setServlet(Servlet servlet) {
        this.servlet = servlet;
    }

    /**
     * サーブレットの設定を返します。
     * 
     * @return サーブレットの設定
     */
    protected MockServletConfig getServletConfig() {
        return servletConfig;
    }

    /**
     * サーブレットの設定を設定します。
     * 
     * @param servletConfig
     *            サーブレットの設定
     */
    protected void setServletConfig(MockServletConfig servletConfig) {
        this.servletConfig = servletConfig;
    }

    /**
     * サーブレットコンテキストを返します。
     * 
     * @return サーブレットコンテキスト
     */
    protected MockServletContext getServletContext() {
        return servletContext;
    }

    /**
     * サーブレットコンテキストを設定します。
     * 
     * @param servletContext
     *            サーブレットコンテキスト
     */
    protected void setServletContext(MockServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * リクエストを返します。
     * 
     * @return リクエスト
     */
    protected MockHttpServletRequest getRequest() {
        return request;
    }

    /**
     * リクエストを設定します。
     * 
     * @param request
     *            リクエスト
     */
    protected void setRequest(MockHttpServletRequest request) {
        this.request = request;
    }

    /**
     * レスポンスを返します。
     * 
     * @return レスポンス
     */
    protected MockHttpServletResponse getResponse() {
        return response;
    }

    /**
     * @param response
     */
    protected void setResponse(MockHttpServletResponse response) {
        this.response = response;
    }

    /**
     * 命名規約を返します。
     * 
     * @return 命名規約
     */
    protected NamingConvention getNamingConvention() {
        return namingConvention;
    }

    /**
     * ターゲット名を返します。
     * 
     * @return ターゲット名
     */
    protected String getTargetName() {
        return getName().substring(4);
    }

    /**
     * ターゲットメソッドを返します。
     * 
     * @return ターゲットメソッド
     */
    protected Method getTargetMethod() {
        return ClassUtil.getMethod(getClass(), getName(), null);
    }

    /**
     * メソッドを実行します。
     * 
     * @param methodName
     *            メソッド名
     * @return 実行結果
     * @throws Throwable
     *             例外が発生した場合
     */
    protected Object invoke(String methodName) throws Throwable {
        try {
            Method method = ClassUtil.getMethod(getClass(), methodName, null);
            return MethodUtil.invoke(method, this, null);
        } catch (NoSuchMethodRuntimeException ignore) {
            return null;
        }
    }

    /**
     * フィールドにコンポーネントをバインドします。
     * 
     * @throws Throwable
     *             例外が発生した場合
     */
    protected void bindFields() throws Throwable {
        boundFields = new ArrayList();
        for (Class clazz = getClass(); clazz != S2FrameworkTestCase.class
                && clazz != null; clazz = clazz.getSuperclass()) {

            Field[] fields = clazz.getDeclaredFields();
            for (int i = 0; i < fields.length; ++i) {
                bindField(fields[i]);
            }
        }
    }

    /**
     * フィールドにコンポーネントをバインドします。
     * 
     * @param field
     *            フィールド
     */
    protected void bindField(Field field) {
        if (isAutoBindable(field)) {
            field.setAccessible(true);
            if (FieldUtil.get(field, this) != null) {
                return;
            }
            String name = normalizeName(field.getName());
            Object component = null;
            if (getContainer().hasComponentDef(name)) {
                Class componentClass = getComponentDef(name)
                        .getComponentClass();
                if (componentClass == null) {
                    component = getComponent(name);
                    if (component != null) {
                        componentClass = component.getClass();
                    }
                }
                if (componentClass != null
                        && field.getType().isAssignableFrom(componentClass)) {
                    if (component == null) {
                        component = getComponent(name);
                    }
                } else {
                    component = null;
                }
            }
            if (component == null
                    && getContainer().hasComponentDef(field.getType())) {
                component = getComponent(field.getType());
            }
            if (component != null) {
                FieldUtil.set(field, this, component);
                boundFields.add(field);
            }
        }
    }

    /**
     * 名前を正規化します。
     * 
     * @param name
     *            名前
     * @return 正規化された名前
     */
    protected String normalizeName(String name) {
        return StringUtil.replace(name, "_", "");
    }

    /**
     * 自動バインディング可能かどうか返します。
     * 
     * @param field
     *            フィールド
     * @return 自動バインディング可能かどうか
     */
    protected boolean isAutoBindable(Field field) {
        int modifiers = field.getModifiers();
        return !Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)
                && !field.getType().isPrimitive();
    }

    /**
     * バインディングされた情報をクリアします。
     */
    protected void unbindFields() {
        for (int i = 0; i < boundFields.size(); ++i) {
            Field field = (Field) boundFields.get(i);
            try {
                field.set(this, null);
            } catch (IllegalArgumentException e) {
                System.err.println(e);
            } catch (IllegalAccessException e) {
                System.err.println(e);
            }
        }
        boundFields = null;
    }

}
