package org.seasar.extension.unit;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;
import javax.transaction.TransactionManager;

import junit.framework.Assert;
import junit.framework.TestCase;

import org.seasar.extension.dataset.DataReader;
import org.seasar.extension.dataset.DataSet;
import org.seasar.extension.dataset.DataTable;
import org.seasar.extension.dataset.DataWriter;
import org.seasar.extension.dataset.impl.SqlDeleteTableWriter;
import org.seasar.extension.dataset.impl.SqlReloadReader;
import org.seasar.extension.dataset.impl.SqlReloadTableReader;
import org.seasar.extension.dataset.impl.SqlTableReader;
import org.seasar.extension.dataset.impl.SqlWriter;
import org.seasar.extension.dataset.impl.XlsReader;
import org.seasar.extension.dataset.impl.XlsWriter;
import org.seasar.extension.jdbc.UpdateHandler;
import org.seasar.extension.jdbc.impl.BasicUpdateHandler;
import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.ContainerConstants;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;
import org.seasar.framework.container.impl.S2ContainerImpl;
import org.seasar.framework.exception.EmptyRuntimeException;
import org.seasar.framework.exception.NoSuchMethodRuntimeException;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.ConnectionUtil;
import org.seasar.framework.util.DataSourceUtil;
import org.seasar.framework.util.FieldUtil;
import org.seasar.framework.util.FileOutputStreamUtil;
import org.seasar.framework.util.MethodUtil;
import org.seasar.framework.util.ResourceUtil;
import org.seasar.framework.util.StringUtil;

public class S2TestCase extends TestCase {

	private static final String DATASOURCE_NAME = "j2ee"
			+ ContainerConstants.NS_SEP + "dataSource";

	private S2Container container_;

	private DataSource dataSource_;
	
	private Connection connection_;
	
	private DatabaseMetaData dbMetaData_;

	private List bindedFields_;

	public S2TestCase(String name) {
		super(name);
	}

	public S2Container getContainer() {
		return container_;
	}

	public Object getComponent(String componentName) {
		return container_.getComponent(componentName);
	}

	public Object getComponent(Class componentClass) {
		return container_.getComponent(componentClass);
	}

	public ComponentDef getComponentDef(String componentName) {
		return container_.getComponentDef(componentName);
	}

	public ComponentDef getComponentDef(Class componentClass) {
		return container_.getComponentDef(componentClass);
	}

	public void register(Class componentClass) {
		container_.register(componentClass);
	}

	public void register(Class componentClass, String componentName) {
		container_.register(componentClass, componentName);
	}

	public void register(Object component) {
		container_.register(component);
	}

	public void register(Object component, String componentName) {
		container_.register(component, componentName);
	}

	public void register(ComponentDef componentDef) {
		container_.register(componentDef);
	}

	public void include(String path) {
		S2ContainerFactory.include(container_, convertPath(path));
	}

	private String convertPath(String path) {
		if (ResourceUtil.getResourceNoException(path) != null) {
			return path;
		} else {
			String prefix = getClass().getPackage().getName().replace('.', '/');
			return prefix + "/" + path;
		}
	}

	public DataSource getDataSource() {
		if (dataSource_ == null) {
			throw new EmptyRuntimeException("dataSource");
		}
		return dataSource_;
	}

	public Connection getConnection() {
		if (connection_ != null) {
			return connection_;
		}
		connection_ = DataSourceUtil.getConnection(getDataSource());
		return connection_;
	}
	
	public DatabaseMetaData getDatabaseMetaData() {
		if (dbMetaData_ != null) {
			return dbMetaData_;
		}
		dbMetaData_ = ConnectionUtil.getMetaData(getConnection());
		return dbMetaData_;
	}

	public DataSet readXls(String path) {
		DataReader reader = new XlsReader(convertPath(path));
		return reader.read();
	}

	public void writeXls(String path, DataSet dataSet) {
		File dir = ResourceUtil.getBuildDir(getClass());
		File file = new File(dir, convertPath(path));
		DataWriter writer = new XlsWriter(FileOutputStreamUtil.create(file));
		writer.write(dataSet);
	}

	public void writeDb(DataSet dataSet) {
		DataWriter writer = new SqlWriter(getDataSource());
		writer.write(dataSet);
	}

	public DataTable readDbByTable(String table) {
		return readDbByTable(table, null);
	}

	public DataTable readDbByTable(String table, String condition) {
		SqlTableReader reader = new SqlTableReader(getDataSource());
		reader.setTable(table, condition);
		return reader.read();
	}

	public DataTable readDbBySql(String sql, String tableName) {
		SqlTableReader reader = new SqlTableReader(getDataSource());
		reader.setSql(sql, tableName);
		return reader.read();
	}

	public void readXlsWriteDb(String path) {
		writeDb(readXls(path));
	}

	public void readXlsReplaceDb(String path) {
		DataSet dataSet = readXls(path);
		deleteDb(dataSet);
		writeDb(dataSet);
	}

	public void readXlsAllReplaceDb(String path) {
		DataSet dataSet = readXls(path);
		for (int i = dataSet.getTableSize() - 1; i >= 0; --i) {
			deleteTable(dataSet.getTable(i).getTableName());
		}
		writeDb(dataSet);
	}

	public DataSet reload(DataSet dataSet) {
		return new SqlReloadReader(getDataSource(), dataSet).read();
	}

	public DataTable reload(DataTable table) {
		return new SqlReloadTableReader(getDataSource(), table).read();
	}

	public void deleteDb(DataSet dataSet) {
		SqlDeleteTableWriter writer = new SqlDeleteTableWriter(getDataSource());
		for (int i = dataSet.getTableSize() - 1; i >= 0; --i) {
			writer.write(dataSet.getTable(i));
		}
	}

	public void deleteTable(String tableName) {
		UpdateHandler handler = new BasicUpdateHandler(getDataSource(),
				"DELETE FROM " + tableName);
		handler.execute(null);
	}

	public void assertEquals(DataSet expected, DataSet actual) {
		assertEquals(null, expected, actual);
	}

	public void assertEquals(String message, DataSet expected, DataSet actual) {

		Assert.assertEquals(message, expected, actual);
	}

	public void assertEquals(DataSet expected, Object actual) {
		assertEquals(null, expected, actual);
	}

	public void assertEquals(String message, DataSet expected, Object actual) {

		Assert.assertNotNull(expected);
		Assert.assertNotNull(actual);

		Class beanClass = null;
		if (actual instanceof List) {
			List actualList = (List) actual;
			Assert.assertFalse(actualList.isEmpty());
			Object actualItem = actualList.get(0);
			if (actualItem instanceof Map) {
				assertMapListEquals(message, expected, actualList);
			} else {
				assertBeanListEquals(message, expected, actualList);
			}
		} else {
			if (actual instanceof Map) {
				assertMapEquals(message, expected, (Map) actual);
			} else {
				assertBeanEquals(message, expected, actual);
			}
		}
	}

	protected void assertMapEquals(String message, DataSet expected, Map map) {

		MapReader reader = new MapReader(map);
		Assert.assertEquals(message, expected, reader.read());
	}

	protected void assertMapListEquals(String message, DataSet expected,
			List list) {

		MapListReader reader = new MapListReader(list);
		Assert.assertEquals(message, expected, reader.read());
	}

	protected void assertBeanEquals(String message, DataSet expected,
			Object bean) {

		BeanReader reader = new BeanReader(bean);
		Assert.assertEquals(message, expected, reader.read());
	}

	protected void assertBeanListEquals(String message, DataSet expected,
			List list) {

		BeanListReader reader = new BeanListReader(list);
		Assert.assertEquals(message, expected, reader.read());
	}

	public void runBare() throws Throwable {
		container_ = new S2ContainerImpl();
		setUp();
		try {
			setUpForEachTestMethod();
			try {
				container_.init();
				try {
					setupDataSource();
					try {
						setUpAfterContainerInit();
						bindFields();
						try {
							runTestTx();
						} finally {
							unbindFields();
						}
						tearDownBeforeContainerDestroy();
					} finally {
						tearDownDataSource();
					}
				} finally {
					container_.destroy();
				}
			} finally {
				tearDownForEachTestMethod();
			}

		} finally {
			tearDown();
		}
	}
	
	protected void setUpAfterContainerInit() throws Throwable {
	}

	protected void setUpForEachTestMethod() throws Throwable {
		invoke("setUp" + getTargetName());
	}

	protected void tearDownBeforeContainerDestroy() throws Throwable {
	}
	
	protected void tearDownForEachTestMethod() throws Throwable {
		invoke("tearDown" + getTargetName());
	}

	private String getTargetName() {
		return getName().substring(4);
	}

	private void invoke(String methodName) throws Throwable {
		try {
			Method method = ClassUtil.getMethod(getClass(), methodName, null);
			MethodUtil.invoke(method, this, null);
		} catch (NoSuchMethodRuntimeException ignore) {
		}
	}

	private void bindFields() throws Throwable {
		bindedFields_ = new ArrayList();
		for (Class clazz = getClass(); clazz != S2TestCase.class
				&& clazz != null; clazz = clazz.getSuperclass()) {

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

	private 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)
					&& field.getType().isAssignableFrom(
							getComponentDef(name)
									.getComponentClass())) {
				component = getComponent(name);
			} else if (getContainer().hasComponentDef(field.getType())) {
				component = getComponent(field.getType());
			}
			if (component != null) {
				FieldUtil.set(field, this, component);
				bindedFields_.add(field);
			}
		}
	}

	private String normalizeName(String name) {
		return StringUtil.replace(name, "_", "");
	}

	private boolean isAutoBindable(Field field) {
		int modifiers = field.getModifiers();
		return !Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)
				&& !field.getType().isPrimitive();
	}

	private void unbindFields() {
		for (int i = 0; i < bindedFields_.size(); ++i) {
			Field field = (Field) bindedFields_.get(i);
			try {
				field.set(this, null);
			} catch (IllegalArgumentException e) {
				System.err.println(e);
			} catch (IllegalAccessException e) {
				System.err.println(e);
			}
		}
	}

	private void runTestTx() throws Throwable {
		TransactionManager tm = null;
		if (needTransaction()) {
			try {
				tm = (TransactionManager) getComponent(TransactionManager.class);
				tm.begin();
			} catch (Throwable t) {
				System.err.println(t);
			}
		}
		try {
			runTest();
		} finally {
			if (tm != null) {
				tm.rollback();
			}
		}
	}

	private boolean needTransaction() {
		return getName().endsWith("Tx");
	}

	private void setupDataSource() {
		try {
			if (container_.hasComponentDef(DATASOURCE_NAME)) {
				dataSource_ = (DataSource) container_
						.getComponent(DATASOURCE_NAME);
			} else if (container_.hasComponentDef(DataSource.class)) {
				dataSource_ = (DataSource) container_
						.getComponent(DataSource.class);
			}
		} catch (Throwable t) {
			System.err.println(t);
		}
	}
	
	private void tearDownDataSource() {
		dbMetaData_ = null;
		if (connection_ != null) {
			ConnectionUtil.close(connection_);
			connection_ = null;
		}
		dataSource_ = null;
	}
}