

package gov.va.med.cds.testharness.xml;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;


/**
 * XmlComparator compares XML documents for equality. Comparison algoritm is not dependent on the order of
 * elements/attributes in the Xml document
 */
public class XmlComparator
{
    private static Log logger = LogFactory.getLog( XmlComparator.class );
    // Used to capture name of non matching element
    private Queue<String> elementNotFound = new LinkedList<String>();

    /** Holds a list of XPath expressions of nodes that should be ignored while making the comparison. */
    private List<String> excludesXPaths;

    private static final String APPLICATION_NAME = "CDS";

    boolean detachedNode;


    /**
     * 
     * @param sourceDocument is the source xml document
     * @param controlDocument is the target xml document
     * @throws XmlCompareException
     */
    private void compareXmlDocuments( Document sourceDocument, Document controlDocument )
        throws XmlCompareException
    {
        Element sourceElement = sourceDocument.getRootElement();
        Element controlElement = controlDocument.getRootElement();

        sourceElement = removeEmptyElementsFromXml( sourceElement );
        controlElement = removeEmptyElementsFromXml( controlElement );

        iterateTree( sourceElement, controlElement );
    }


    private void compareXmlDocumentsAfterRemovingAllEmptyElements( Document sourceDocument, Document controlDocument )
        throws XmlCompareException
    {
        Element sourceElement = sourceDocument.getRootElement();
        Element controlElement = controlDocument.getRootElement();

        detachedNode = true;
        while ( detachedNode == true )
        {
            detachedNode = false;
            sourceElement = removeEmptyElementsFromXml( sourceElement );
        }

        detachedNode = true;
        while ( detachedNode == true )
        {
            detachedNode = false;
            controlElement = removeEmptyElementsFromXml( controlElement );
        }

        iterateTree( sourceElement, controlElement );
    }


    /**
     * Iterate XML Dom tree to access elements' attributes and values. The tree is iterated recursively.
     * 
     * @param sourceElement is the root element from source document
     * @param controlElement is the root element from target document
     * @throws XmlCompareException
     */
    @SuppressWarnings( "unchecked" )
    private void iterateTree( Element sourceElement, Element controlElement )
        throws XmlCompareException
    {
        if ( this.excludesXPaths == null || ( this.excludesXPaths != null && !this.excludesXPaths.contains( sourceElement.getPath() ) ) )
        {
            compareElementName( sourceElement, controlElement );
            compareElementValue( sourceElement, controlElement );
            compareAttributeLists( sourceElement.getUniquePath(), sourceElement.attributes(), controlElement.attributes() );
            compareChildCount( sourceElement, controlElement );

            List<Element> sourceSubElementsList = new LinkedList<Element>();
            List<Element> controlSubElementsList = new LinkedList<Element>();

            // Iterate through child elements of root, source & target
            for ( Iterator input = sourceElement.elementIterator(); input.hasNext(); )
            {
                Element ipElement = ( Element )input.next();
                sourceSubElementsList.add( ipElement );
            }

            for ( Iterator output = controlElement.elementIterator(); output.hasNext(); )
            {
                Element opElement = ( Element )output.next();
                controlSubElementsList.add( opElement );
            }

            Iterator sourceIterator = sourceSubElementsList.iterator();

            // Iterate one sub-element at a time
            while ( sourceIterator.hasNext() )
            {
                boolean foundAMatch = false;
                Element sourceSubElement = ( Element )sourceIterator.next();

                // Find all elements in control list that have the same name.
                List<Element> controlWithSameNameElementList = getElementsWithSameName( sourceSubElement.getQualifiedName(), controlSubElementsList );
                if ( controlWithSameNameElementList.size() == 0 )
                {
                    String msg = "A source element : " + sourceElement.getUniquePath() + " could not be found in the control xml.";
                    if ( logger.isDebugEnabled() )
                    {
                        logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
                    }
                    throw new XmlCompareException( msg );
                }

                Iterator controlIterator = controlWithSameNameElementList.iterator();
                while ( controlIterator.hasNext() )
                {
                    Element controlSubElement = ( Element )controlIterator.next();
                    try
                    {
                        iterateTree( sourceSubElement, controlSubElement );
                        foundAMatch = true;
                        // Remove the elements which are equal so that you dont use
                        // them for comparision again
                        sourceSubElementsList.remove( sourceSubElement );
                        controlWithSameNameElementList.remove( controlSubElement );
                        // Regenerate iterators to reflect change in list size
                        sourceIterator = sourceSubElementsList.iterator();
                        controlIterator = controlWithSameNameElementList.iterator();
                        break;
                    }
                    catch ( XmlCompareException e )
                    {
                        // Don't care about the failures. We are only looking for
                        // successes.
                        elementNotFound.offer( e.getMessage() );
                    }
                }
                if ( !foundAMatch )
                {
                    throw new XmlCompareException( elementNotFound.remove() );
                }
            }
        }
    }


    /**
     * Determines elements that qualify to be compared in control xml.
     * 
     * @param qualifiedName is the xpath to the element that is being compared
     * @param controlSubElementsList is the list of elements in the control xml
     * @return a list of elements in control xml that need to be compared
     */
    private List<Element> getElementsWithSameName( String qualifiedName, List<Element> controlSubElementsList )
    {
        List<Element> result = new LinkedList<Element>();
        Iterator controlIterator = controlSubElementsList.iterator();
        while ( controlIterator.hasNext() )
        {
            Element controlSubElement = ( Element )controlIterator.next();
            if ( qualifiedName.equals( controlSubElement.getQualifiedName() ) )
                result.add( controlSubElement );
        }
        return result;
    }


    /**
     * Compares the number of elements in source xml and control element
     * 
     * @param sourceElement
     * @param controlElement
     * @throws XmlCompareException when the number of elements are not equal
     */
    private void compareChildCount( Element sourceElement, Element controlElement )
        throws XmlCompareException
    {
        if ( sourceElement.elements().size() != controlElement.elements().size() )
        {
            String msg = "The child counts were not equal at: " + sourceElement.getUniquePath() + " source was: " + sourceElement.elements().size()
                            + " control was: " + controlElement.elements().size();
            if ( logger.isDebugEnabled() )
            {
                logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
            }
            throw new XmlCompareException( msg );
        }
    }


    /**
     * Compares the value of the source xml and control element
     * 
     * @param sourceElement is the source element being compared for its value
     * @param controlElement is the control element being compared for its value
     * @throws XmlCompareException when the values are different
     */
    private void compareElementValue( Element sourceElement, Element controlElement )
        throws XmlCompareException
    {
        if ( !sourceElement.getTextTrim().equals( controlElement.getTextTrim() ) )
        {
            String msg = "The element values were not equal at: " + sourceElement.getUniquePath() + " source was: " + sourceElement.getText()
                            + " control was: " + controlElement.getText();
            if ( logger.isDebugEnabled() )
            {
                logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
            }
            throw new XmlCompareException( msg );
        }
    }


    /**
     * Compares the name of the source xml and control element
     * 
     * @param sourceElement
     * @param controlElement
     * @throws XmlCompareException
     */
    private void compareElementName( Element sourceElement, Element controlElement )
        throws XmlCompareException
    {
        if ( !sourceElement.getQualifiedName().equals( controlElement.getQualifiedName() ) )
        {
            String msg = "The element names were not equal at: " + sourceElement.getUniquePath() + " source was: " + sourceElement.getQualifiedName()
                            + " control was: " + controlElement.getQualifiedName();
            if ( logger.isDebugEnabled() )
            {
                logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
            }

            throw new XmlCompareException( msg );
        }
    }


    /**
     * Returns iterator for the Attribute List
     * 
     * @param attributeList is the list of attributes in the element
     * @return a reference to the attribute list
     */
    private Iterator getAttListIterator( List attributeList )
    {
        Iterator iterator = attributeList.iterator();

        return iterator;
    }


    /**
     * Compares source attribute list against the control attribute list
     * 
     * @param parentPath is the absolute path os the element whose attributes are being compared
     * @param sourceList is the list source element attributes
     * @param controlList is the list of target element attributes
     * @return the result of comparison
     * @throws XmlCompareException
     */
    private boolean compareAttributeLists( String parentPath, List sourceList, List controlList )
        throws XmlCompareException
    {
        if ( sourceList != null && controlList == null )
        {
            String msg = "for node: " + parentPath + " the source attribute list is not null, but the contol attribute list is null.";
            if ( logger.isDebugEnabled() )
            {
                logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
            }
            throw new XmlCompareException( msg );
        }

        if ( sourceList == null && controlList != null )
        {
            String msg = "for node: " + parentPath + " the source attribute list is null, but the contol attribute list is not null.";
            if ( logger.isDebugEnabled() )
            {
                logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
            }
            throw new XmlCompareException( msg );
        }

        sourceList = removeExcludedAttributes( sourceList );
        controlList = removeExcludedAttributes( controlList );

        if ( sourceList.size() != controlList.size() )
        {
            String msg = "for node: " + parentPath + " the source attribute list length was: " + sourceList.size()
                            + ", but the contol attribute list length was: " + controlList.size();
            if ( logger.isDebugEnabled() )
            {
                logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
            }
            throw new XmlCompareException( msg );
        }

        compareSourceAttributeListToControlAttributeList( sourceList, controlList );

        return true;
    }


    @SuppressWarnings( "unchecked" )
    private List removeExcludedAttributes( List attributeList )
    {
        List prunedAttributeList = new ArrayList( attributeList );
        if ( this.excludesXPaths == null )
        {
            return prunedAttributeList;
        }

        Attribute attribute = null;

        for ( Iterator i = attributeList.iterator(); i.hasNext(); )
        {
            attribute = ( Attribute )i.next();
            if ( this.excludesXPaths.contains( attribute.getPath() ) )
            {
                prunedAttributeList.remove( attribute );
            }
        }

        return prunedAttributeList;
    }


    /**
     * Compares the source list and the control list as a cartesian product
     * 
     * @param source
     * @param control
     * @throws XmlCompareException
     */
    @SuppressWarnings( "unchecked" )
    private void compareSourceAttributeListToControlAttributeList( List source, List control )
        throws XmlCompareException
    {
        List sourceList = new LinkedList( source );
        List controlList = new LinkedList( control );

        Iterator sourceListIterator = sourceList.iterator();
        Iterator targetListIterator = controlList.iterator();

        boolean attListsEqual = false;
        while ( sourceListIterator.hasNext() )
        {
            Attribute sourceAttribute = ( Attribute )sourceListIterator.next();

            attListsEqual = false;

            while ( targetListIterator.hasNext() )
            {
                Attribute targetAttribute = ( Attribute )targetListIterator.next();

                if ( ( sourceAttribute.getText().equals( targetAttribute.getText() ) )
                                && ( sourceAttribute.getName().equals( targetAttribute.getName() ) ) )
                {
                    attListsEqual = true;
                    // Remove attributes which pass equality, so that you do not
                    // use them for comparision again
                    controlList.remove( targetAttribute );
                    sourceList.remove( sourceAttribute );
                    // Iterator has to be generated to reflect change in list
                    // size
                    targetListIterator = getAttListIterator( controlList );
                    sourceListIterator = getAttListIterator( sourceList );

                    break;
                }
            }
            // If source sub element was not found in target Xml, then exit
            // comparison
            if ( !attListsEqual )
            {
                String msg = "A match could not be found in the contol xml for source attribute " + sourceAttribute.getUniquePath() + "=\""
                                + sourceAttribute.getText() + "\"";
                if ( logger.isDebugEnabled() )
                {
                    logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, msg ) );
                }
                throw new XmlCompareException( msg );
            }
        }
    }


    /**
     * Compares source xml and control xml for equality
     * 
     * @param sourceXml is the source xml
     * @param controlXml is the control xml
     * @throws XmlCompareException if the xml strings are unequal
     */
    public static void assertXMLSimilar( String sourceXml, String controlXml )
        throws XmlCompareException
    {
        if ( logger.isDebugEnabled() )
        {
            logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, "Control: " + controlXml ) );
            logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, "Source: " + sourceXml ) );
        }

        XmlComparator xmlComparator = new XmlComparator();
        xmlComparator.compareXmlDocuments( xmlComparator.xmlStringXmlDocument( sourceXml ), xmlComparator.xmlStringXmlDocument( controlXml ) );
    }


    public static void assertXMLSimilar( String sourceXml, String controlXml, String preProcess )
        throws XmlCompareException
    {
        if ( logger.isDebugEnabled() )
        {
            logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, "Control: " + controlXml ) );
            logger.debug( gov.va.med.cds.util.LogMessageUtil.buildMessage( null, null, APPLICATION_NAME, "Source: " + sourceXml ) );
        }

        XmlComparator xmlComparator = new XmlComparator();
        xmlComparator.compareXmlDocuments( xmlComparator.xmlStringXmlDocument( sourceXml ), xmlComparator.xmlStringXmlDocument( controlXml ) );
    }


    /**
     * Converts a XML string to XML Document object
     * 
     * @param xml data is input as a string
     * @return document element representing the XML
     * @throws XmlCompareException when it is possible to transform the XML string to a XML document
     */
    private Document xmlStringXmlDocument( String xml )
        throws XmlCompareException
    {
        Document document;

        try
        {
            document = DocumentHelper.parseText( xml );
        }
        catch ( DocumentException e )
        {
            throw new XmlCompareException( "Problem with parsing the xml test source:\n" + xml, e );
        }

        return document;
    }


    public static void assertXMLSimilar( String sourceXml, String controlXml, List<String> excludes )
        throws XmlCompareException
    {

    	XmlComparator xmlComparator = new XmlComparator();
        Document sourceDocument = xmlComparator.xmlStringXmlDocument( sourceXml );
        Document controlDocument = xmlComparator.xmlStringXmlDocument( controlXml );
        xmlComparator.setExcludesXPaths( excludes );

        
        xmlComparator.compareXmlDocuments( sourceDocument, controlDocument );
    }


    public static void assertXMLSimilarAfterRemovingAllEmptyElements( String sourceXml, String controlXml, List<String> excludes )
        throws XmlCompareException
    {

    	XmlComparator xmlComparator = new XmlComparator();
        Document sourceDocument = xmlComparator.xmlStringXmlDocument( sourceXml );
        Document controlDocument = xmlComparator.xmlStringXmlDocument( controlXml );
        xmlComparator.setExcludesXPaths( excludes );

        xmlComparator.compareXmlDocumentsAfterRemovingAllEmptyElements( sourceDocument, controlDocument );
    }


    private void setExcludesXPaths( List<String> excludesXPaths )
    {
        this.excludesXPaths = excludesXPaths;
    }


    /**
     * Remove elements with no/blank text value from DOM objects. The VDM builder creates a blank element with no value
     * instead of not creating the element. This method will overlook this VDM Builder behavior
     * 
     * @param source is the XML element that will be pruned of elements with empty text fields
     * @return Element after pruning elements with empty text fields
     */
    private Element removeEmptyElementsFromXml( Element source )
    {

        for ( int counter = 0, size = source.nodeCount(); counter < size; counter++ )
        {
            Node node = source.node( counter );

            if ( node instanceof Element )
            {

                Element nodeElement = ( Element )node;

                if ( ( !node.hasContent() ) && ( nodeElement.attributeCount() == 0 ) )
                {
                    node.detach();
                    // The size of the branch has changed and needs to be reset
                    size = source.nodeCount();
                    counter-- ;
                    detachedNode = true;
                }
                else
                {
                    removeEmptyElementsFromXml( ( Element )node );
                }
            }
        }

        return source;

    }
}
