/*
  This file is licensed to You 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 org.xmlunit.matchers;

import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.isEmptyString;
import static org.junit.Assert.assertThat;
import static org.xmlunit.TestResources.TEST_RESOURCE_DIR;
import static org.xmlunit.matchers.CompareMatcher.isIdenticalTo;
import static org.xmlunit.matchers.CompareMatcher.isSimilarTo;

import org.xmlunit.XMLUnitException;
import org.xmlunit.builder.Input;
import org.xmlunit.builder.Input.Builder;
import org.xmlunit.diff.Comparison;
import org.xmlunit.diff.Comparison.Detail;
import org.xmlunit.diff.ComparisonFormatter;
import org.xmlunit.diff.ComparisonListener;
import org.xmlunit.diff.ComparisonResult;
import org.xmlunit.diff.ComparisonType;
import org.xmlunit.diff.DefaultNodeMatcher;
import org.xmlunit.diff.DifferenceEvaluator;
import org.xmlunit.diff.ElementSelectors;
import org.xmlunit.util.Predicate;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matchers;
import org.hamcrest.StringDescription;
import org.junit.Assert;
import org.junit.ComparisonFailure;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class CompareMatcherTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    /** set this to true for manual review of the Error Messages, how the really looks in your IDE. */
    private final boolean letExceptionTestFail = false;

    @Test
    public void testIsIdenticalTo_withAssertionErrorForAttributes_throwsReadableMessage() {
        // Expected Exception
        expect(AssertionError.class);
        expectMessage("Expected attribute value 'xy' but was 'xyz'");
        expectMessage("at /Element[1]/@attr2");
        expectMessage("attr2=\"xyz\"");

        // run test:
        assertThat("<Element attr2=\"xyz\" attr1=\"12\"/>", isIdenticalTo("<Element attr1=\"12\" attr2=\"xy\"/>"));
    }

    @Test
    public void testIsIdenticalTo_withAssertionErrorForElementOrder_throwsReadableMessage() {
        // Expected Exception
        expect(AssertionError.class);
        expectMessage("Expected child nodelist sequence '0' but was '1'");
        expectMessage("comparing <b...> at /a[1]/b[1] to <b...> at /a[1]/b[1]");

        // run test:
        assertThat("<a><c/><b/></a>", isIdenticalTo("<a><b/><c/></a>")
            .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
    }

    @Test
    public void testIsIdenticalTo_withAssertionErrorForWhitespaces_throwsReadableMessage() {
        // Expected Exception
        expect(AssertionError.class);
        expectMessage("Expected child nodelist length '1' but was '3'");
        expectMessage("<a>" + getLineSeparator()  + "  <b/>" + getLineSeparator()  + "</a>");
        expectMessage("<a><b/></a>");

        // run test:
        assertThat("<a>\n  <b/>\n</a>", isIdenticalTo("<a><b/></a>"));
    }

    @Test
    public void testIsIdenticalTo_withComparisonFailureForWhitespaces_throwsReadableMessage() {
        // Expected Exception
        expect(ComparisonFailure.class);
        expectMessage("Expected child nodelist length '1' but was '3'");
        expectMessage("expected:<<a>[<b/>]</a>> but was:<<a>[" + getLineSeparator()  + "  <b/>" + getLineSeparator()  + "]</a>>");

        // run test:
        assertThat("<a>\n  <b/>\n</a>", isIdenticalTo("<a><b/></a>").throwComparisonFailure());
    }

    @Test
    public void testIsIdenticalTo_withIgnoreWhitespaces_shouldSucceed() {

        // run test:
        assertThat("<a>\n  <b/>\n</a>", isIdenticalTo("<a><b/></a>").ignoreWhitespace());
    }

    @Test
    public void testIsIdenticalTo_withIgnoreElementContentWhitespaces_shouldSucceed() {

        // run test:
        assertThat("<a>\n  <b/>\n</a>", isIdenticalTo("<a><b/></a>").ignoreElementContentWhitespace());
    }

    @Test
    public void testIsIdenticalTo_withIgnoreComments_shouldSucceed() {

        // run test:
        assertThat("<a><!-- test --></a>", isIdenticalTo("<a></a>").ignoreComments());
    }

    @Test
    public void testIsIdenticalTo_withIgnoreComments1_0_shouldSucceed() {

        // run test:
        assertThat("<a><!-- test --></a>", isIdenticalTo("<a></a>").ignoreCommentsUsingXSLTVersion("1.0"));
    }

    @Test
    public void testIsIdenticalTo_withIgnoreComments2_0_shouldSucceed() {

        // run test:
        assertThat("<a><!-- test --></a>", isIdenticalTo("<a></a>").ignoreCommentsUsingXSLTVersion("2.0"));
    }

    @Test
    public void testIsIdenticalTo_withNormalizeWhitespace_shouldSucceed() {

        // run test:
        assertThat("<a>\n  <b>\n  Test\n  Node\n  </b>\n</a>", isIdenticalTo("<a><b>Test Node</b></a>")
            .normalizeWhitespace());
    }

    @Test
    public void testIsIdenticalTo_withNormalizeWhitespace_shouldFail() {

        expect(AssertionError.class);
        expectMessage("Expected text value 'TestNode' but was 'Test Node'");

        // run test:
        assertThat("<a>\n  <b>\n  Test\n  Node\n  </b>\n</a>", isIdenticalTo("<a><b>TestNode</b></a>")
            .normalizeWhitespace());
    }

    @Test
    public void testIsSimilarTo_withSwappedElements_shouldSucceed() {
        // run test:
        assertThat("<a><c/><b/></a>", isSimilarTo("<a><b/><c/></a>")
            .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
    }

    @Test
    public void testIsSimilarTo_withFileInput() {
        expect(AssertionError.class);
        expectMessage("In Source");
        expectMessage("test2.xml");

        // run test:
        assertThat(new File(TEST_RESOURCE_DIR, "test1.xml"),
                   isSimilarTo(new File(TEST_RESOURCE_DIR, "test2.xml")));
    }

    @Test
    public void testIsSimilarTo_withDifferenceEvaluator_shouldSucceed() {
        // prepare testData
        final String control = "<a><b attr=\"abc\"></b></a>";
        final String test = "<a><b attr=\"xyz\"></b></a>";

        // run test
        assertThat(test, isSimilarTo(control).withDifferenceEvaluator(new IgnoreAttributeDifferenceEvaluator("attr")));

    }

    @Test
    public void testIsSimilarTo_withComparisonFormatter_shouldFailWithCustomMessage() {
        // prepare testData
        expect(AssertionError.class);
        expectMessage("DESCRIPTION");
        expectMessage("DETAIL-abc");
        expectMessage("DETAIL-xyz");

        final String control = "<a><b attr=\"abc\"></b></a>";
        final String test = "<a><b attr=\"xyz\"></b></a>";

        // run test
        assertThat(test, isSimilarTo(control).withComparisonFormatter(new DummyComparisonFormatter()));

    }

    @Test
    public void testIsSimilarTo_withComparisonFormatterAndThrowComparisonFailure_shouldFailWithCustomMessage() {
        // prepare testData
        expect(ComparisonFailure.class);
        expectMessage("DESCRIPTION");
        expectMessage("DETAIL-[abc]");
        expectMessage("DETAIL-[xyz]");

        final String control = "<a><b attr=\"abc\"></b></a>";
        final String test = "<a><b attr=\"xyz\"></b></a>";

        // run test
        assertThat(test, isSimilarTo(control).withComparisonFormatter(new DummyComparisonFormatter()).throwComparisonFailure());

    }

    @Test
    public void testIsSimilarTo_withComparisonListener_shouldCollectChanges() {
        CounterComparisonListener comparisonListener = new CounterComparisonListener();
        String controlXml = "<a><b>Test Value</b><c>ABC</c></a>";
        String testXml = "<a><b><![CDATA[Test Value]]></b><c>XYZ</c></a>";

        // run test:
        try {
            assertThat(testXml, isSimilarTo(controlXml).withComparisonListeners(comparisonListener));
        } catch (AssertionError e) {
            assertThat(e.getMessage(), containsString("Expected text value 'ABC' but was 'XYZ'"));
        }
        
        // validate result
        assertThat(comparisonListener.differents, is(1));
        assertThat(comparisonListener.similars, is(1));
        assertThat(comparisonListener.equals, is(Matchers.greaterThan(10)));
    }

    @Test
    public void testIsSimilarTo_withDifferenceListener_shouldCollectChanges() {
        CounterComparisonListener comparisonListener = new CounterComparisonListener();
        String controlXml = "<a><b>Test Value</b><c>ABC</c></a>";
        String testXml = "<a><b><![CDATA[Test Value]]></b><c>XYZ</c></a>";

        // run test:
        try {
            assertThat(testXml, isSimilarTo(controlXml).withDifferenceListeners(comparisonListener));
            Assert.fail("Should throw AssertionError");
        } catch (AssertionError e) {
            assertThat(e.getMessage(), containsString("Expected text value 'ABC' but was 'XYZ'"));
        }
        
        // validate result
        assertThat(comparisonListener.differents, is(1));
        assertThat(comparisonListener.similars, is(1));
        assertThat(comparisonListener.equals, is(0));
    }

    @Test
    public void testCompareMatcherWrapper_shouldWriteFailedTestInput() {
        
        final String control = "<a><b attr=\"abc\"></b></a>";
        final String test = "<a><b attr=\"xyz\"></b></a>";

        // run test
        final String fileName = "testCompareMatcherWrapper.xml";
        try {
            assertThat(test, TestCompareMatcherWrapper.isSimilarTo(control).withTestFileName(fileName));
            Assert.fail("Should throw AssertionError");
        } catch (AssertionError e) {
            assertThat(e.getMessage(), containsString("Expected attribute value 'abc' but was 'xyz'"));
        }
        
        // validate that the written File contains the right data:
        assertThat(new File(getTestResultFolder(), fileName), isSimilarTo(test));
    }

    @Test
    public void testDiff_withAttributeDifferences() {
        final String control = "<a><b attr1=\"abc\" attr2=\"def\"></b></a>";
        final String test = "<a><b attr1=\"xyz\" attr2=\"def\"></b></a>";

        try {
            assertThat(test, isSimilarTo(control));
            Assert.fail("Should throw AssertionError");
        } catch (AssertionError e) {
            assertThat(e.getMessage(), containsString("Expected attribute value 'abc' but was 'xyz'"));
        }

        assertThat(test, isSimilarTo(control)
                   .withAttributeFilter(new Predicate<Attr>() {
                           @Override
                           public boolean test(Attr a) {
                               return !"attr1".equals(a.getName());
                           }
                       }));
    }

    @Test
    public void testDiff_withExtraNodes() {
        String control = "<a><b></b><c/></a>";
        String test = "<a><b></b><c/><d/></a>";

        try {
            assertThat(test, isSimilarTo(control));
        } catch (AssertionError e) {
            assertThat(e.getMessage(), containsString("Expected child nodelist length '2' but was '3'"));
        }

        assertThat(test,
                   isSimilarTo(control)
                   .withNodeFilter(new Predicate<Node>() {
                           @Override
                           public boolean test(Node n) {
                               return !"d".equals(n.getNodeName());
                           }
                       }));
    }

    /**
     * Really only tests there is no NPE.
     * @see "https://github.com/xmlunit/xmlunit/issues/81"
     */
    @Test(expected = AssertionError.class)
    public void canBeCombinedWithFailingMatcher() {
        assertThat("not empty", both(isEmptyString()).and(isIdenticalTo("")));
    }

    @Test
    public void canBeCombinedWithPassingMatcher() {
        assertThat("<a><c/><b/></a>", both(not(isEmptyString()))
                   .and(isSimilarTo("<a><b/><c/></a>")
                        .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))));
    }

    @Test
    public void usesDocumentBuilderFactory() throws Exception {
        DocumentBuilderFactory dFac = Mockito.mock(DocumentBuilderFactory.class);
        DocumentBuilder b = Mockito.mock(DocumentBuilder.class);
        Mockito.when(dFac.newDocumentBuilder()).thenReturn(b);
        Mockito.doThrow(new IOException())
            .when(b).parse(Mockito.any(InputSource.class));

        String control = "<a><b></b><c/></a>";
        String test = "<a><b></b><c/><d/></a>";

        try {
            assertThat("<a><b></b><c/></a>",
                       not(isSimilarTo("<a><b></b><c/><d/></a>")
                           .withDocumentBuilderFactory(dFac)));
            Assert.fail("Expected exception");
        } catch (XMLUnitException ex) {
            Mockito.verify(b).parse(Mockito.any(InputSource.class));
        }
    }

    @Test(expected = UnsupportedOperationException.class)
    public void cantSetComparisonController() {
        isSimilarTo("<foo/>").withComparisonController(null);
    }

    /**
     * @see "https://github.com/xmlunit/xmlunit/issues/107"
     */
    @Test
    public void describeToWorksWhenThereAreNoDifferences() throws Exception {
        CompareMatcher m = isIdenticalTo("<foo/>");
        Assert.assertTrue(m.matches("<foo/>"));
        StringDescription sd = new StringDescription();
        m.describeTo(sd);
        assertThat(sd.toString(), is(" is equal to the control document"));
    }

    @Test
    public void createsAUsefulMessageWhenFailingCombinedWithNot() throws Exception {
        expect(AssertionError.class);
        expectMessage("not  is similar to the control document");
        assertThat("<a><b></b><c/></a>", not(isSimilarTo("<a><b></b><c/></a>")));
    }

    public void expect(Class<? extends Throwable> type) {
        if (letExceptionTestFail) return;
        thrown.expect(type);
    }

    public void expectMessage(String substring) {
        if (letExceptionTestFail) return;
        thrown.expectMessage(substring);
    }

    private String getLineSeparator() {
        return System.getProperty("line.separator");
    }

    private final class DummyComparisonFormatter implements ComparisonFormatter {

        @Override
        public String getDetails(Detail details, ComparisonType type, boolean formatXml) {
            return "DETAIL-" + details.getValue();
        }

        @Override
        public String getDescription(Comparison difference) {
            return "DESCRIPTION";
        }
    }

    private final class IgnoreAttributeDifferenceEvaluator implements DifferenceEvaluator {

        private final String attributeName;

        public IgnoreAttributeDifferenceEvaluator(String attributeName) {
            this.attributeName = attributeName;
        }

        @Override
        public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
            final Node controlNode = comparison.getControlDetails().getTarget();
            if (controlNode instanceof Attr) {
                Attr attr = (Attr) controlNode;
                if (attr.getName().equals(attributeName)) {
                    return ComparisonResult.EQUAL;
                }
            }
            return outcome;
        }
    }

    private final class CounterComparisonListener implements ComparisonListener {

        private int equals;
        private int similars;
        private int differents;

        @Override
        public void comparisonPerformed(Comparison comparison, ComparisonResult outcome) {
            switch (outcome) {
            case EQUAL:
                equals++;
                break;
            case SIMILAR:
                similars++;
                break;
            case DIFFERENT:
                differents++;
                break;

            default:
                break;
            }
        }

    }

    /**
     * Example Wrapper for {@link CompareMatcher}.
     * <p>
     * This example will write the Test-Input into the Files System.<br>
     * This could be useful for manual reviews or as template for a control-File.
     * 
     */
    private static class TestCompareMatcherWrapper extends BaseMatcher<Object> {
        private final CompareMatcher compareMatcher;
        private String fileName;
        protected TestCompareMatcherWrapper(CompareMatcher compareMatcher) {
            this.compareMatcher = compareMatcher;
        }
        public TestCompareMatcherWrapper withTestFileName(String fileName) {
            this.fileName = fileName;
            return this;
        }

        public static TestCompareMatcherWrapper isSimilarTo(final Object control) {
            return new TestCompareMatcherWrapper(CompareMatcher.isSimilarTo(control));
        }

        @Override
        public boolean matches(Object testItem) {
            if (fileName == null) {
                return compareMatcher.matches(testItem);
            }
            // do something with your Test-Source
            final Builder builder = Input.from(testItem);
            // e.g.: write the testItem into the FilesSystem. So it can be used as template for a new control-File.
            final File testFile = writeIntoTestResultFolder(builder.build());
            return compareMatcher.matches(testFile);
        }

        private File writeIntoTestResultFolder(final Source source) throws TransformerFactoryConfigurationError {
            File file = new File(getTestResultFolder(), this.fileName);
            try {
                if (!file.exists()) {
                    file.createNewFile();
                }
                try (FileOutputStream fop = new FileOutputStream(file)) {
                    marshal(source, fop);
                    fop.flush();
                }
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            return file;
        }

        private void marshal(final Source source, FileOutputStream fop)
                throws TransformerFactoryConfigurationError, TransformerConfigurationException, TransformerException {
            StreamResult r = new StreamResult(fop);
            TransformerFactory fac = TransformerFactory.newInstance();
            Transformer t = fac.newTransformer();
            t.transform(source, r);
        }

        @Override
        public void describeTo(Description description) {
            compareMatcher.describeTo(description);
        }

        @Override
        public void describeMismatch(Object item, Description description) {
            compareMatcher.describeMismatch(item, description);
        }
        
    }

    private static File getTestResultFolder() {
        final File folder = new File(//
            String.format("./target/testResults/%s", CompareMatcherTest.class.getSimpleName()));
        if (!folder.exists()) {
            folder.mkdirs();
        }
        return folder;
    }
}
