/*
 * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
 * Copyright (C) 2011, 2013-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

package com.github.javaparser.printer;

import static com.github.javaparser.StaticJavaParser.parseExpression;
import static org.junit.jupiter.api.Assertions.fail;

import com.github.javaparser.ast.expr.Expression;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

class XmlPrinterTest {

    // Used for building XML documents
    private static DocumentBuilderFactory documentBuilderFactory;
    private static DocumentBuilder documentBuilder;

    @BeforeAll
    public static void setupDocumentBuilder() {
        try {
            documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            documentBuilderFactory.setCoalescing(true);
            documentBuilderFactory.setIgnoringElementContentWhitespace(true);
            documentBuilderFactory.setIgnoringComments(true);
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException ex) {
            throw new RuntimeException(ex);
        }
    }

    // Used for serializing XML documents (Necessary only when doing error reporting)
    private static TransformerFactory transformerFactory;
    private static Transformer transformer;

    @BeforeAll
    public static void setupTransformerFactory() {
        try {
            transformerFactory = TransformerFactory.newInstance();
            transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        } catch (TransformerConfigurationException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Set of cleanups to be done at the end of each test execution.
     */
    private Set<Cleanup> cleanupSet;

    /**
     * Add given runnable to set of elements to be called.
     *
     * @param cleanup Object to be called at the end of each test execution
     */
    private void onEnd(Cleanup cleanup) {
        cleanupSet.add(cleanup);
    }

    @BeforeEach
    public void clearCleanupSet() {
        cleanupSet = new HashSet<>();
    }

    @AfterEach
    public void runCleanup() throws Exception {
        for (Cleanup cleanup : cleanupSet) {
            cleanup.run();
        }
    }

    public Document getDocument(String xml) throws SAXException, IOException {
        InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
        Document result = documentBuilder.parse(inputStream);
        result.normalizeDocument();
        return result;
    }

    public String getXML(Document document) throws TransformerException {
        StringWriter result = new StringWriter(); // Closing a StringWriter is not needed
        transformer.transform(new DOMSource(document), new StreamResult(result));
        return result.toString();
    }

    private File createTempFile() throws IOException {
        File result = File.createTempFile("javaparser", "test.xml");
        onEnd(result::delete); // Schedule file deletion at the end of Test
        return result;
    }

    public void assertXMLEquals(String expected, String actual) throws SAXException, IOException {
        final Document expectedDocument = getDocument(expected);
        final Document actualDocument = getDocument(actual);

        if (!expectedDocument.isEqualNode(actualDocument)) {
            try {
                fail(String.format("-- expected:\n%s-- actual:\n%s", getXML(expectedDocument), getXML(actualDocument)));
            } catch (TransformerException ex) {
                fail(
                        String.format(
                                ""
                                        + "expected: <%s>, but it was <%s>\n"
                                        + "Additionally, a TransformerException was raised when trying to report XML document contents",
                                expected, actual),
                        ex);
            }
        }
    }

    @Test
    void testWithType() throws SAXException, IOException {
        Expression expression = parseExpression("1+1");
        XmlPrinter xmlOutput = new XmlPrinter(true);

        String output = xmlOutput.output(expression);

        assertXMLEquals(
                "<root type='BinaryExpr' operator='PLUS'><left type='IntegerLiteralExpr' value='1'></left><right type='IntegerLiteralExpr' value='1'></right></root>",
                output);
    }

    @Test
    void testWithoutType() throws SAXException, IOException {
        Expression expression = parseExpression("1+1");

        XmlPrinter xmlOutput = new XmlPrinter(false);

        String output = xmlOutput.output(expression);

        assertXMLEquals("<root operator='PLUS'><left value='1'></left><right value='1'></right></root>", output);
    }

    @Test
    void testList() throws SAXException, IOException {
        Expression expression = parseExpression("a(1,2)");

        XmlPrinter xmlOutput = new XmlPrinter(true);

        String output = xmlOutput.output(expression);

        assertXMLEquals(
                "<root type='MethodCallExpr'><name type='SimpleName' identifier='a'></name><arguments><argument type='IntegerLiteralExpr' value='1'></argument><argument type='IntegerLiteralExpr' value='2'></argument></arguments></root>",
                output);
    }

    // Demonstrate the use of streaming, without use of temporary strings.
    @Test
    void testStreamToFile() throws SAXException, IOException, XMLStreamException {

        File tempFile = createTempFile();

        try (FileWriter fileWriter = new FileWriter(tempFile)) {
            XmlPrinter xmlOutput = new XmlPrinter(false);
            xmlOutput.outputDocument(parseExpression("1+1"), "root", fileWriter);
        }

        assertXMLEquals(
                ""
                        // Expected
                        + "<root operator='PLUS'>"
                        + "<left value='1'/>"
                        + "<right value='1'/>"
                        + "</root>",
                // Actual (Using temporary string for checking results. No one has been used when generating XML)
                new String(Files.readAllBytes(tempFile.toPath())));
    }

    @Test
    void testCustomXML() throws SAXException, IOException, XMLStreamException {

        StringWriter stringWriter = new StringWriter();

        XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
        XMLStreamWriter xmlWriter = outputFactory.createXMLStreamWriter(stringWriter);
        onEnd(xmlWriter::close);

        XmlPrinter xmlOutput = new XmlPrinter(false);

        xmlWriter.writeStartDocument();
        xmlWriter.writeStartElement("custom");

        xmlOutput.outputNode(parseExpression("1+1"), "plusExpr", xmlWriter);
        xmlOutput.outputNode(parseExpression("a(1,2)"), "callExpr", xmlWriter);

        xmlWriter.writeEndElement();
        xmlWriter.writeEndDocument();
        xmlWriter.close();

        assertXMLEquals(
                ""
                        // Expected
                        + "<custom>"
                        + "<plusExpr operator='PLUS'>"
                        + "<left value='1'/>"
                        + "<right value='1'/>"
                        + "</plusExpr>"
                        + "<callExpr>"
                        + "<name identifier='a'/>"
                        + "<arguments>"
                        + "<argument value='1'/>"
                        + "<argument value='2'/>"
                        + "</arguments>"
                        + "</callExpr>"
                        + "</custom>",
                // Actual
                stringWriter.toString());
    }

    @Test
    void testAbsentTypeParameterList() throws SAXException, IOException, XMLStreamException {
        Expression expression = parseExpression("new HashSet()");
        XmlPrinter xmlOutput = new XmlPrinter(false);
        String output = xmlOutput.output(expression);
        assertXMLEquals(
                ""
                        // Expected
                        + "<root>"
                        + "<type>"
                        + "<name identifier='HashSet'/>"
                        + "</type>"
                        + "</root>",
                // Actual
                output);
    }

    @Test
    void testEmptyTypeParameterList() throws SAXException, IOException, XMLStreamException {
        Expression expression = parseExpression("new HashSet<>()");
        XmlPrinter xmlOutput = new XmlPrinter(false);
        String output = xmlOutput.output(expression);
        assertXMLEquals(
                ""
                        // Expected
                        + "<root>"
                        + "<type>"
                        + "<name identifier='HashSet'/>"
                        + "<typeArguments/>"
                        + "</type>"
                        + "</root>",
                // Actual
                output);
    }

    @Test
    void testNonEmptyTypeParameterList() throws SAXException, IOException, XMLStreamException {
        Expression expression = parseExpression("new HashSet<Integer,File>()");
        XmlPrinter xmlOutput = new XmlPrinter(false);
        String output = xmlOutput.output(expression);
        assertXMLEquals(
                ""
                        // Expected
                        + "<root>"
                        + "<type>"
                        + "<name identifier='HashSet'/>"
                        + "<typeArguments>"
                        + "<typeArgument>"
                        + "<name identifier='Integer'/>"
                        + "</typeArgument>"
                        + "<typeArgument>"
                        + "<name identifier='File'/>"
                        + "</typeArgument>"
                        + "</typeArguments>"
                        + "</type>"
                        + "</root>",
                // Actual
                output);
    }
}

interface Cleanup {
    void run() throws Exception;
}
