/*
 * 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.IOException;
import java.io.OutputStreamWriter;
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.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 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){
			System.out.print("-- report start -- expected --");
			print(c);
			System.out.print("-- but --");
			print(context);
			System.out.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){
			System.out.print("-- equals start -- expected --");
			System.out.print(a);
			System.out.print("-- but --");
			System.out.print(b);
			System.out.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 key:a.keySet()){
			if(!equalsPrivate(a.get(key), b.get(key))){
				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);
	}
}
