/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * 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 woolpack.test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import woolpack.dom.DomContext;
import woolpack.dom.DomExpression;
import woolpack.dom.XmlToNode;
import woolpack.utils.StringReaderFactory;
import woolpack.utils.UtilsConstants;

/**
 * テスト用の静的メソッドの集まり。
 * 
 * @author nakamura
 * 
 */
public final class TestUtils {
	private static final Pattern QUERY = Pattern
			.compile("[\\?&]?([^=\\?]+)=([^&]*)");

	private static final String FORM_HEAD = "<HTML><BODY><FORM>";

	private static final String FORM_TAIL = "</FORM></BODY></HTML>";

	private static final String BODY_HEAD = "<HTML><BODY>";

	private static final String BODY_TAIL = "</BODY></HTML>";

	private TestUtils() {
	} // カバレージがここを通過してはいけない

	private static void addTo(final Map<String, List<String>> result,
			final String key, final String value) {
		List<String> list = result.get(key);
		if (list == null) {
			list = new ArrayList<String>();
			result.put(key, list);
		}
		list.add(value);
	}

	/**
	 * URL のクエリーからキーとその値を抽出する。
	 * 
	 * @param query
	 *            URL。
	 * @return 抽出されたキー(属性値)と値のコレクションの{@link Map}。
	 */
	public static Map<String, List<String>> selectQuery(final String query) {
		// TODO URL クエリーの処理を厳密化する。
		final Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
		final Matcher m = QUERY.matcher(query);
		while (m.find()) {
			addTo(result, m.group(1), m.group(2));
		}
		return result;
	}

	private static String getText(final Node node) {
		final StringBuilder sb = new StringBuilder();
		final NodeList nodeList = node.getChildNodes();
		for (int i = 0; i < nodeList.getLength(); i++) {
			if (nodeList.item(i).getNodeType() == Node.TEXT_NODE) {
				sb.append(nodeList.item(i).getNodeValue());
			}
		}
		return sb.toString();
	}

	private static void selectForm(final Node node,
			final Map<String, List<String>> result, final String selectName) {
		final String name;
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final Element e = (Element) node;
			final String elementName = e.getNodeName();
			final String value;
			if ("OPTION".equals(elementName)) {
				name = selectName;
				value = e.hasAttribute("selected") ? e.getAttribute("value")
						: null;
			} else if (e.hasAttribute("name")) {
				name = e.getAttribute("name");
				if ("SELECT".equals(elementName)) {
					value = null;
				} else if ("INPUT".equals(elementName)) {
					final String inputType = e.getAttribute("type");
					if ("radio".equals(inputType)
							|| "checkbox".equals(inputType)) {
						if (e.hasAttribute("checked")) {
							value = e.hasAttribute("value") ? e
									.getAttribute("value") : "";
						} else {
							value = null;
						}
					} else {
						value = e.hasAttribute("value") ? e
								.getAttribute("value") : "";
					}
				} else if ("BUTTON".equals(elementName)) {
					value = e.hasAttribute("value") ? e.getAttribute("value")
							: "";
				} else {
					value = getText(e);
				}
			} else {
				name = selectName;
				value = null;
			}
			if (value != null) {
				addTo(result, name, value);
			}
		} else {
			name = null;
		}
		final NodeList nodeList = node.getChildNodes();
		for (int i = 0; i < nodeList.getLength(); i++) {
			final Node n = nodeList.item(i);
			if (n.getNodeType() == Node.ELEMENT_NODE) {
				selectForm(n, result, name);
			}
		}
	}

	/**
	 * DOMノードをフォームとしてキーとその値を抽出する。
	 * 
	 * @param node
	 *            抽出対象。
	 * @return 抽出されたキー(属性値)と値のコレクションの{@link Map}。
	 */
	public static Map<String, List<String>> selectForm(final Node node) {
		final Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
		selectForm(node, result, null);
		return result;
	}

	/**
	 * DOM ノードからキーとその値を抽出する。 属性名の一覧のいずれかを属性名として持つ DOM エレメントを検索し、
	 * 属性値をキー、子テキストノードを全て結合した結果を値として抽出する。
	 * 
	 * @param attrNames
	 *            属性名の一覧。本クラスはこの引数の状態を変化させない。
	 * @param node
	 *            抽出対象。
	 * @return 抽出されたキー(属性値)と値のコレクションの{@link Map}。
	 */
	public static Map<String, List<String>> selectEmbedded(
			final Iterable<String> attrNames, final Node node) {
		final Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
		selectEmbedded(attrNames, node, result);
		return result;
	}

	private static void selectEmbedded(final Iterable<String> attrNames,
			final Node node, final Map<String, List<String>> result) {
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final Element e = (Element) node;
			for (final String attrName : attrNames) {
				if (!e.hasAttribute(attrName)) {
					continue;
				}
				addTo(result, e.getAttribute(attrName), getText(e));
				break;
			}
		}
		final NodeList nodeList = node.getChildNodes();
		for (int i = 0; i < nodeList.getLength(); i++) {
			selectEmbedded(attrNames, nodeList.item(i), result);
		}
	}

	/**
	 * {@link DomContext#getNode()}を標準出力に出力する。
	 * 
	 * @param context
	 *            出力対象。
	 * @throws NullPointerException
	 *             引数または{@link DomContext#getNode()}が null の場合。
	 */
	public static void print(final DomContext context) {
		UtilsConstants.TRANSFORMER_FACTORY.newInstance().transform(
				new DOMSource(context.getNode()),
				new StreamResult(new OutputStreamWriter(System.out)));
	}

	/**
	 * 引数を XML の文字列表現とみなして親ノードとして /HTML/BODY/FORM/ を付加して DOM ノードを生成する{@link DomExpression}を返す。
	 * 
	 * @param s
	 *            HTML-BODY-FORM の子ノードの XML 表現。
	 * @return ノードを生成する{@link DomExpression}。
	 */
	public static DomExpression getToNodeForm(final String s) {
		return getToNode(FORM_HEAD + s + FORM_TAIL);
	}

	/**
	 * 引数を XML の文字列表現とみなして親ノードとして /HTML/BODY/ を付加して DOM ノードを生成する{@link DomExpression}を返す。
	 * 
	 * @param s
	 *            HTML-BODY の子ノードの XML 表現。
	 * @return ノードを生成する{@link DomExpression}。
	 */
	public static DomExpression getToNodeBody(final String s) {
		return getToNode(BODY_HEAD + s + BODY_TAIL);
	}

	/**
	 * 引数を XML の文字列表現とみなして DOM ノードを生成する{@link DomExpression}を返す。
	 * 
	 * @param s
	 *            XML 表現。
	 * @return ノードを生成する{@link DomExpression}。
	 */
	public static DomExpression getToNode(final String s) {
		return new XmlToNode(new StringReaderFactory(s));
	}
	

	/**
	 * 引数 expected を XML の文字列表現とみなして親ノードに /HTML/BODY/FORM/ を付加して DOM ノードを比較する。
	 * 
	 * @param context
	 *            比較先。
	 * @param expected
	 *            比較元。
	 * @return 比較元と比較先が同一内容を表すなら true。それ以外は false。
	 */
	public static boolean equalsForm(final DomContext context,
			final String expected) {
		return equals(context, FORM_HEAD + expected + FORM_TAIL);
	}

	/**
	 * 引数 expected を XML の文字列表現とみなして親ノードに /HTML/BODY/ を付加して DOM ノードを比較する。
	 * 
	 * @param context
	 *            比較先。
	 * @param expected
	 *            比較元。
	 * @return 比較元と比較先が同一内容を表すなら true。それ以外は false。
	 */
	public static boolean equalsBody(final DomContext context,
			final String expected) {
		return equals(context, BODY_HEAD + expected + BODY_TAIL);
	}

	/**
	 * 引数 expected を XML の文字列表現とみなして DOM ノードを比較する。
	 * 
	 * @param context
	 *            比較先。
	 * @param expected
	 *            比較元。
	 * @return 比較元と比較先が同一内容を表すなら true。それ以外は false。
	 */
	public static boolean equals(final DomContext context, final String expected) {
		final DomContext c = new DomContext();
		getToNode(expected).interpret(c);
		context.getNode().normalize();
		c.getNode().normalize();
		final boolean result = equals(c.getNode(), context.getNode());
		if (!result) {
			synchronized(TestUtils.class){
				final PrintStream stream = System.out;
				stream.print("-- report start -- expected --");
				print(c);
				stream.print("-- but --");
				print(context);
				stream.println("-- report end --");
			}
		}
		return result;
	}

	/**
	 * DOM ノードを比較する。 同一の内容でない場合は引数を標準出力する。
	 * 
	 * @param node0
	 *            比較元。
	 * @param node1
	 *            比較先。
	 * @return 比較元と比較先が同一内容を表すなら true。それ以外は false。
	 */
	public static boolean equals(final Node node0, final Node node1) {
		if (node0 == null) {
			return node1 == null;
		}
		if (node1 == null) {
			return false;
		}
		if (node0.getNodeType() != node1.getNodeType()) {
			return false;
		}
		if (node0.getNodeType() == Node.TEXT_NODE
				|| node0.getNodeType() == Node.COMMENT_NODE) {
			return node0.getNodeValue().equals(node1.getNodeValue());
		}
		if (node0.getNodeType() == Node.ATTRIBUTE_NODE) {
			return node0.getNodeName().equals(node1.getNodeName())
					&& node0.getNodeValue().equals(node1.getNodeValue());
		}
		if (node0.getNodeType() == Node.DOCUMENT_NODE) {
			return equals(((Document) node0).getDocumentElement(),
					((Document) node1).getDocumentElement());
		}
		if (!node0.getNodeName().equals(node1.getNodeName())) {
			return false;
		}

		final Element e0 = (Element) node0;
		final Element e1 = (Element) node1;
		final NamedNodeMap map0 = e0.getAttributes();
		final NamedNodeMap map1 = e1.getAttributes();
		if (map0.getLength() != map1.getLength()) {
			return false;
		}
		for (int i = 0; i < map0.getLength(); i++) {
			if (!equals(map0.item(i), map1.item(i))) {
				return false;
			}
		}

		Node child0 = node0.getFirstChild();
		Node child1 = node1.getFirstChild();
		while (child0 != null || child1 != null) {
			if (!equals(child0, child1)) {
				return false;
			}
			child0 = child0.getNextSibling();
			child1 = child1.getNextSibling();
		}
		return true;
	}

	/**
	 * {@link Reader}を読み込んで文字列に変換し、{@link Reader#close()}する。
	 * 
	 * @param reader
	 *            読み込み先。
	 * @return 引数の内容。
	 * @throws IOException
	 *             {@link Reader#read()}に失敗した場合。
	 */
	public static String toString(final Reader reader) throws IOException {
		try {
			final StringBuilder sb = new StringBuilder();
			int i = 0;
			while ((i = reader.read()) >= 0) {
				sb.append((char) i);
			}
			return sb.toString();
		} finally {
			reader.close();
		}
	}

	/**
	 * 構造化されたコレクションを再帰的に比較する。 同一の内容でない場合は引数を標準出力する。 引数が両方とも{@link List}の場合、各々同一インデックスの要素が同一の場合にふたつの{@link List}を同一とする。
	 * 配列は{@link List}とみなして同一性を判定する。 両方とも{@link Collection}であり片方が{@link List}でも配列でもない場合は順序に関係ない{@link Collection}の包含関係を比較する。
	 * 引数が両方とも{@link Map}の場合、{@link Map#keySet()}が同一で各々同一キーに対する値が同一の場合にふたつの{@link Map}を同一とする。
	 * 引数の両方とも{@link LinkedHashMap}の場合は要素の出現順序を同一性判定に含める。 引数の両方とも{@link Map}で片方が{@link LinkedHashMap}でない場合は要素の出現順序を同一性判定に含めない。
	 * 
	 * @param a
	 *            比較元。
	 * @param b
	 *            比較先。
	 * @return 比較結果。
	 */
	public static boolean equals(final Object a, final Object b) {
		final boolean result = equalsPrivate(a, b);
		if (!result) {
			final PrintStream stream = System.out;
			stream.print("-- equals start -- expected --");
			stream.print(a);
			stream.print("-- but --");
			stream.print(b);
			stream.println("-- equals end --");
		}
		return result;
	}

	private static boolean equalsPrivate(final Object a, final Object b) {
		if (a == null) {
			return b == null;
		} else {
			if (b == null) {
				return false;
			}
		}
		if ((a instanceof LinkedHashMap) && (b instanceof LinkedHashMap)) {
			final LinkedHashMap aMap = (LinkedHashMap) a;
			final LinkedHashMap bMap = (LinkedHashMap) b;
			if (!equalsMap(aMap, bMap)) {
				return false;
			}
			final Iterator aIterator = aMap.keySet().iterator();
			final Iterator bIterator = bMap.keySet().iterator();
			while (aIterator.hasNext()) {
				if (!equalsPrivate(aIterator.next(), bIterator.next())) {
					return false;
				}
			}
			return true;
		}
		if (a instanceof Map) {
			if (b instanceof Map) {
				final Map aMap = (Map) a;
				final Map bMap = (Map) b;
				return equalsMap(aMap, bMap);
			} else {
				return false;
			}
		} else {
			if (b instanceof Map) {
				return false;
			}
		}
		if ((a instanceof List || a.getClass().isArray())
				&& (b instanceof List || a.getClass().isArray())) {
			final List aList = UtilsConstants.toList(a);
			final List bList = UtilsConstants.toList(b);
			if (aList.size() != bList.size()) {
				return false;
			}
			final int length = aList.size();
			for (int i = 0; i < length; i++) {
				if (!equalsPrivate(aList.get(i), bList.get(i))) {
					return false;
				}
			}
			return true;
		}
		if ((a instanceof Collection) && (b instanceof Collection)) {
			return equalsCollection((Collection) a, (Collection) b);
		}
		return a.equals(b);
	}

	private static boolean equalsMap(final Map a, final Map b) {
		if (!equalsCollection(a.keySet(), b.keySet())) {
			return false;
		}
		for (final Object entryObject : a.entrySet()) {
			final Entry entry = (Entry) entryObject;
			if (!equalsPrivate(entry.getValue(), b.get(entry.getKey()))) {
				return false;
			}
		}
		return true;
	}

	private static boolean equalsCollection(final Collection a,
			final Collection b) {
		return containsAll(a, b) && containsAll(b, a);
	}

	private static boolean containsAll(final Collection a, final Collection b) {
		return a.containsAll(b);
	}

	/**
	 * 引数を直列化し、再び復元したものを返す。
	 * @param from 直列化対象。
	 * @return 引数を直列化し、再び復元したもの。
	 * @throws IOException 直列化に失敗した場合。
	 * @throws ClassNotFoundException 直列化に失敗した場合。
	 */
	public static Object sandwitchSerial(final Object from) throws IOException, ClassNotFoundException {
		final ByteArrayOutputStream baos = new ByteArrayOutputStream();
		final ObjectOutputStream oos = new ObjectOutputStream(baos);
		try {
			oos.writeObject(from);
		} finally {
			oos.close();
		}
		final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
		try {
			return ois.readObject();
		} finally {
			ois.close();
		}
	}
}
