package org.biomoby.shared;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.custommonkey.xmlunit.DetailedDiff;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.Difference;
import org.custommonkey.xmlunit.ElementNameAndTextQualifier;
import org.custommonkey.xmlunit.ElementNameQualifier;
import org.custommonkey.xmlunit.NamespaceContext;
import org.custommonkey.xmlunit.SimpleNamespaceContext;
import org.custommonkey.xmlunit.XMLUnit;
import org.custommonkey.xmlunit.XpathEngine;
import org.custommonkey.xmlunit.exceptions.XpathException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * A class representing a moby unit test. In addition, this class has methods to
 * perform the various tests required for unit testing moby services.
 * 
 * @author Wendy Alexander
 * @author Eddie Kawas
 * 
 * @version $Id: MobyUnitTest.java,v 1.3 2009/02/03 16:39:41 kawas Exp $
 */
public class MobyUnitTest {

    // string representing an example input to a service
    private String exampleInput;

    // string representing valid output
    private String validOutputXML;

    // a regular expression that can be applied to the output of a service
    private String validREGEX;

    // an xpath statement that can be used to drill into the output of a service
    private String validXPath;

    /**
     * Default Constructor - creates a blank MobyUnitTest object
     */
    public MobyUnitTest() {
	setExampleInput("");
	setValidOutputXML("");
	setValidREGEX("");
	setValidXPath("");
	init();
    }

    /*
     * set up XMLUnit for doing our comparisons
     * 
     * we would like to: -> ignore attribute order because it doesnt matter ->
     * ignore XML comments, because they are irrelevant in moby -> ignore
     * element whitespace because it doesnt change semantics -> treat CDATA
     * sections and text nodes alike
     * 
     */
    private void init() {
	// for comparing dom trees
	XMLUnit.setIgnoreAttributeOrder(true);
	XMLUnit.setIgnoreComments(true);
	XMLUnit.setIgnoreWhitespace(true);
	XMLUnit.setIgnoreDiffBetweenTextAndCDATA(true);
    }

    /**
     * Getter: get the example input that you can use to invoke this service
     * 
     * @return a string of XML representing a services example input
     */
    public String getExampleInput() {
	return exampleInput;
    }

    /**
     * Setter: set the example input that can be passed to the service to invoke
     * it
     * 
     * @param exampleInput
     *                a string of XML representing a services example input
     */
    public void setExampleInput(String exampleInput) {
	if (exampleInput != null)
	    this.exampleInput = exampleInput;
    }

    /**
     * Getter: get the expected XML output for this service (usually by sending
     * it the input from <code>getExampleInput()</code>
     * 
     * @return a string of XML representing a services expected output
     */
    public String getValidOutputXML() {
	return validOutputXML;
    }

    /**
     * Setter: set the expected XML output for this serivce
     * 
     * @param validOutputXML
     *                a string of XML representing a services expected output
     */
    public void setValidOutputXML(String validOutputXML) {
	if (validOutputXML != null)
	    this.validOutputXML = validOutputXML;
    }

    /**
     * Getter: get the Regular Expression for this unit test
     * 
     * @return a regular expression that can be used on a services output to
     *         determine validity
     */
    public String getValidREGEX() {
	return validREGEX;
    }

    /**
     * Setter: set the regular expression for this unit test
     * 
     * @param validREGEX
     *                a regular expression that can be used on a services output
     *                to determine validity
     */
    public void setValidREGEX(String validREGEX) {
	if (validREGEX != null)
	    this.validREGEX = validREGEX;
    }

    /**
     * Getter: get the xpath expression for this unit test
     * 
     * @return an xpath expression that can be used to drill into a services
     *         output
     */
    public String getValidXPath() {
	return validXPath;
    }

    /**
     * Setter: set the xpath expression for this unit test
     * 
     * @param validXPath
     *                an xpath expression that can be used to drill into a
     *                services output
     */
    public void setValidXPath(String validXPath) {
	if (validXPath != null)
	    this.validXPath = validXPath;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    public String toString() {
	StringBuffer buf = new StringBuffer();
	buf.append("Unit Test:\n");
	buf.append("   Input       \n" + Utils.format(getExampleInput(), 2)
		+ "\n");
	buf.append("   Output      \n" + Utils.format(getValidOutputXML(), 2)
		+ "\n");
	buf.append("   XPath       \n" + Utils.format(getValidXPath(), 2)
		+ "\n");
	buf.append("   REGEX       \n" + Utils.format(getValidREGEX(), 2)
		+ "\n");
	// return buf.toString();
	return Utils.format(buf.toString(), 1);
    }

    /**
     * XML is assumed to be wellformed and valid. Comparing 'XML' that is not
     * wellformed may be identical but this method will return false!
     * 
     * @param testXML
     *                the XML that you would like to compare to XML obtained
     *                from <code>getValidOutputXML()</code>
     * @return true if the documents are semantically similar, false otherwise
     * 
     * @throws MobyException
     *                 if there is a problem reading/parsing the testXML
     */
    public boolean compareOutputXML(String testXML) throws MobyException {
	if (!getValidOutputXML().trim().equals(""))
	    try {
		Diff diff;
		diff = new Diff(getValidOutputXML(), testXML);
		// diff.overrideElementQualifier(new
		// RecursiveElementNameAndTextQualifier());
		diff.overrideElementQualifier(new MobyQualifier());
		return diff.similar();
	    } catch (SAXException e) {
		throw new MobyException(e.getMessage());
	    } catch (IOException e) {
		throw new MobyException(e.getMessage());
	    }
	return false;
    }

    /**
     * 
     * @param testXML
     *                the XML that you would like to test the XPATH, from
     *                <code>getValidXPath()</code>, expression against
     * @return true if the XPath expression matches at least one node in the
     *         testXML
     * @throws MobyException
     *                 if there is a problem compiling the XPATH expression, or
     *                 reading/parsing the textXML
     */
    public boolean compareXmlWithXpath(String testXML) throws MobyException {
	if (!getValidXPath().trim().equals(""))
	    try {
		Document d = XMLUnit.buildControlDocument(testXML);
		HashMap<String, String> m = new HashMap<String, String>();
		if (d.getDocumentElement() != null) {
		    if (d.getDocumentElement().getPrefix() != null)
			m.put(d.getDocumentElement().getPrefix(), d
				.getDocumentElement().getNamespaceURI());
		}
		NamespaceContext ctx = new SimpleNamespaceContext(m);
		XpathEngine engine = XMLUnit.newXpathEngine();
		engine.setNamespaceContext(ctx);
		return engine.getMatchingNodes(getValidXPath(), d).getLength() > 0;
	    } catch (IOException ioe) {
		throw new MobyException("IOException:\n", ioe);
	    } catch (SAXException se) {
		throw new MobyException("Invalid XML:\n", se);
	    } catch (XpathException xe) {
		throw new MobyException("Invalid XPATH:\n" + xe);
	    }
	return false;
    }

    /**
     * 
     * @param testXML
     *                the XML that you would like to test the REGEX, from
     *                <code>getValidREGEX()</code>, expression against
     * @return true if the regular expression matches the testXML, false
     *         otherwise.
     */
    public boolean compareXmlWithREGEX(String testXML, boolean multiline) {
	if (!getValidREGEX().trim().equals("")) {
	    Pattern p = multiline ? Pattern.compile(getValidREGEX(),
		    Pattern.MULTILINE) : Pattern.compile(getValidREGEX());
	    Matcher m = p.matcher(testXML);
	    return m.find();
	}
	return false;
    }

    /**
     * 
     * @param testXML
     *                the XML that you would like to 'diff' to XML obtained from
     *                <code>getValidOutputXML()</code>
     * @return a String of text outlining all of the differences
     */
    @SuppressWarnings("unchecked")
    public String getXMLDifferences(String testXML) {
	if (!getValidOutputXML().trim().equals(""))
	    try {
		StringBuilder sb = new StringBuilder();
		DetailedDiff myDiff = new DetailedDiff(new Diff(
			getValidOutputXML(), testXML));
		// myDiff.overrideElementQualifier(new
		// RecursiveElementNameAndTextQualifier());
		myDiff.overrideElementQualifier(new MobyQualifier());
		List<Difference> differences = myDiff.getAllDifferences();
		for (Difference d : differences)
		    sb.append(d.toString() + "\n");
		return sb.toString();
	    } catch (SAXException e) {
		return e.getMessage();
	    } catch (IOException e) {
		return e.getMessage();
	    }
	return "No XML to validate against!";
    }

    private static final String[] ALL_ATTRIBUTES = { "*" };

    /**
     * This inner class implements a 'Qualifier' used to compare XML documents
     * using the XMLUnit engine. It used both
     * RecursiveElementNameAndTextQualifier and
     * MultiLevelElementNameAndTextQualifier as a template.
     * 
     * @author Eddie Kawas
     * 
     */
    protected class MobyQualifier extends ElementNameQualifier {
	/*
	 * *****************************************************************
	 * Copyright (c) 2008, Jeff Martin, Tim Bacon All rights reserved.
	 * 
	 * Redistribution and use in source and binary forms, with or without
	 * modification, are permitted provided that the following conditions
	 * are met:
	 * 
	 * Redistributions of source code must retain the above copyright
	 * notice, this list of conditions and the following disclaimer.
	 * Redistributions in binary form must reproduce the above copyright
	 * notice, this list of conditions and the following disclaimer in the
	 * documentation and/or other materials provided with the distribution.
	 * Neither the name of the xmlunit.sourceforge.net nor the names of its
	 * contributors may be used to endorse or promote products derived from
	 * this software without specific prior written permission.
	 * 
	 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
	 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
	 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
	 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
	 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
	 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
	 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
	 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
	 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
	 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
	 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	 * 
	 * *****************************************************************
	 */
	private final String[] qualifyingAttrNames;

	private final ElementNameQualifier NAME_QUALIFIER = new ElementNameQualifier();

	private final ElementNameAndTextQualifier NAME_AND_TEXT_QUALIFIER = new ElementNameAndTextQualifier();

	/**
	 * No-args constructor: use all attributes from all elements to
	 * determine whether elements qualify for comparability
	 */
	public MobyQualifier() {
	    this(ALL_ATTRIBUTES);
	}

	/**
	 * Simple constructor for a single qualifying attribute name
	 * 
	 * @param attrName
	 *                the value to use to qualify whether two elements can
	 *                be compared further for differences
	 */
	public MobyQualifier(String attrName) {
	    this(new String[] { attrName });
	}

	/**
	 * Extended constructor for multiple qualifying attribute names
	 * 
	 * @param attrNames
	 *                the array of values to use to qualify whether two
	 *                elements can be compared further for differences
	 */
	public MobyQualifier(String[] attrNames) {
	    this.qualifyingAttrNames = new String[attrNames.length];
	    System.arraycopy(attrNames, 0, qualifyingAttrNames, 0,
		    attrNames.length);
	}

	/**
	 * Determine whether two elements qualify for further Difference
	 * comparison.
	 * 
	 * @param differenceEngine
	 *                the DifferenceEngine instance wanting to determine if
	 *                the elements are comparable
	 * @param control
	 * @param test
	 * @return true if the two elements qualify for further comparison based
	 *         on both the superclass qualification (namespace URI and non-
	 *         namespaced tag name), and the presence of qualifying
	 *         attributes with the same values; false otherwise
	 */
	public boolean qualifyForComparison_old(Element control, Element test) {
	    if (super.qualifyForComparison(control, test)) {
		return areAttributesComparable(control, test);
	    }
	    return false;
	}

	public boolean qualifyForComparison(Element control, Element test) {
	    boolean stillSimilar = true;
	    Element currentControl = control;
	    Element currentTest = test;

	    // we dont try to compare the Collection node because they have
	    // unordered children
	    if (!control.getLocalName().equals("Collection")) {
		// not a collection, match name and attributes
		stillSimilar = NAME_QUALIFIER.qualifyForComparison(
			currentControl, currentTest);

		if (stillSimilar) {
		    if (currentControl.hasChildNodes()
			    && currentTest.hasChildNodes()) {
			Node n1 = getFirstEligibleChild(currentControl);
			Node n2 = getFirstEligibleChild(currentTest);
			if (n1.getNodeType() == Node.ELEMENT_NODE
				&& n2.getNodeType() == Node.ELEMENT_NODE) {
			    currentControl = (Element) n1;
			    currentTest = (Element) n2;
			} else {
			    stillSimilar = false;
			}
		    } else {
			stillSimilar = false;
		    }
		}

		// finally compare the level containing the text child node
		if (stillSimilar) {
		    stillSimilar = NAME_AND_TEXT_QUALIFIER
			    .qualifyForComparison(currentControl, currentTest);
		}
	    }
	    return stillSimilar

	    && areAttributesComparable(currentControl, currentTest);

	}

	private Node getFirstEligibleChild(Node parent) {
	    Node n1 = parent.getFirstChild();
	    while (n1.getNodeType() == Node.TEXT_NODE
		    && n1.getNodeValue().trim().length() == 0) {
		Node n2 = n1.getNextSibling();
		if (n2 == null)
		    break;
		n1 = n2;
	    }
	    return n1;
	}

	/**
	 * Determine whether the qualifying attributes are present in both
	 * elements and if so whether their values are the same
	 * 
	 * @param control
	 * @param test
	 * @return true if all qualifying attributes are present with the same
	 *         values, false otherwise
	 */
	protected boolean areAttributesComparable(Element control, Element test) {
	    String controlValue, testValue;
	    Attr[] qualifyingAttributes;
	    NamedNodeMap namedNodeMap = control.getAttributes();
	    if (matchesAllAttributes(qualifyingAttrNames)) {
		qualifyingAttributes = new Attr[namedNodeMap.getLength()];
		for (int n = 0; n < qualifyingAttributes.length; ++n) {
		    qualifyingAttributes[n] = (Attr) namedNodeMap.item(n);
		}
	    } else {
		qualifyingAttributes = new Attr[qualifyingAttrNames.length];
		for (int n = 0; n < qualifyingAttrNames.length; ++n) {
		    qualifyingAttributes[n] = (Attr) namedNodeMap
			    .getNamedItem(qualifyingAttrNames[n]);
		}
	    }

	    String nsURI, name;
	    for (int i = 0; i < qualifyingAttributes.length; ++i) {
		if (qualifyingAttributes[i] != null) {
		    nsURI = qualifyingAttributes[i].getNamespaceURI();
		    controlValue = qualifyingAttributes[i].getNodeValue();
		    name = qualifyingAttributes[i].getName();
		} else {
		    // cannot be "*" case
		    nsURI = controlValue = "";
		    name = qualifyingAttrNames[i];
		}
		if (nsURI == null || nsURI.length() == 0) {
		    testValue = test.getAttribute(name);
		} else {
		    testValue = test.getAttributeNS(nsURI,
			    qualifyingAttributes[i].getLocalName());
		}
		if (controlValue == null) {
		    if (testValue != null) {
			return false;
		    }
		} else {
		    if (!controlValue.equals(testValue)) {
			return false;
		    }
		}
	    }
	    return true;
	}

	private boolean matchesAllAttributes(String[] attributes) {
	    return Arrays.equals(attributes, ALL_ATTRIBUTES);
	}

    }
}
