/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of 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.
 * This program 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.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.gatein.common.xml.stax;

import static org.mockito.Mockito.*;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamWriter;

import junit.framework.TestCase;

import org.gatein.common.xml.stax.writer.StaxWriter;
import org.gatein.common.xml.stax.writer.StaxWriterImpl;
import org.gatein.common.xml.stax.writer.WritableValueType;
import org.gatein.common.xml.stax.writer.formatting.XmlStreamingFormatter;
import org.mockito.InOrder;
import org.staxnav.Naming;

public abstract class AbstractStaxWriterTest<N> extends TestCase {
    static final String ENCODING = "UTF-8";
    static final String VERSION = "1.0";

    private Naming<N> naming;
    protected XMLStreamWriter stream;
    protected XmlStreamingFormatter formatter;
    protected StaxWriter<N> writer;

    public abstract Naming<N> getNaming();

    @Override
    public void setUp() throws Exception {
        naming = getNaming();
        formatter = mock(XmlStreamingFormatter.class);
        stream = mock(XMLStreamWriter.class);
        writer = new StaxWriterImpl<N>(naming, stream, formatter, ENCODING, VERSION);
    }

    protected N createName(String localPart) {
        return naming.getName(null, null, localPart);
    }

    public void testEncodingAndVersion() throws Exception {
        String encoding = "UTF-8";
        String version = "1.0";
        StaxWriter<N> writer = new StaxWriterImpl<N>(naming, stream, formatter, encoding, version);
        writer.writeElement(createName("foo"), "blah");

        verify(stream).writeStartDocument(encoding, version);

        reset(stream);
        encoding = "encoding";
        version = "version";
        writer = new StaxWriterImpl<N>(naming, stream, formatter, encoding, version);
        writer.writeElement(createName("foo"), "blah");

        verify(stream).writeStartDocument(encoding, version);
    }

    public void testEncoding() throws Exception {
        String encoding = "ISO-8859-1";
        StaxWriter<N> writer = new StaxWriterImpl<N>(naming, stream, formatter, encoding, null);
        writer.writeElement(createName("foo"), "blah");

        verify(stream).writeStartDocument(encoding, null);
    }

    public void testNoEncoding() throws Exception {
        StaxWriter<N> writer = new StaxWriterImpl<N>(naming, stream, formatter);
        writer.writeElement(createName("foo"), "blah");

        verify(stream).writeStartDocument();
    }

    public void testStartDocument() throws Exception {
        writer.writeStartElement(createName("foo"));
        writer.writeComment("some comment");
        writer.writeCData("some cdata < & ...");

        verify(stream, times(1)).writeStartDocument(ENCODING, VERSION);

    }

    public void testWriteStartElement() throws Exception {
        writer.writeStartElement(createName("foo"));
        verify(stream).writeStartElement("foo");
    }

    public void testWriteAttribute() throws Exception {
        writer.writeStartElement(createName("foo")).writeAttribute("name", "value");

        verify(stream).writeAttribute("name", "value");
    }

    public void testWriteQNameAttribute() throws Exception {
        writer.writeStartElement(createName("foo")).writeAttribute(new QName("uri", "local", "pre"), "value")
                .writeAttribute(new QName("uri", "local"), "value");

        verify(stream).writeAttribute("pre", "uri", "local", "value");
        verify(stream).writeAttribute("uri", "local", "value");
    }

    public void testWriteElement() throws Exception {
        writer.writeElement(createName("foo"), "content");

        verify(stream).writeStartElement("foo");
        verify(stream).writeCharacters("content");
        verify(stream).writeEndElement();
    }

    public void testWriteContent() throws Exception {

        writer.writeStartElement(createName("foo")).writeContent("blah");
        verify(stream).writeCharacters("blah");

        WritableValueType<String> vt = new WritableValueType<String>() {
            @Override
            public String format(String value) {
                return "some value";
            }
        };

        writer.writeStartElement(createName("bar")).writeContent(vt, "content");
        verify(stream).writeCharacters("some value");
    }

    public void testWriteNullContent() throws Exception {
        try {
            writer.writeContent(null);
            fail("IllegalArgumentException expected to be thrown");
        } catch (IllegalArgumentException e) {
        }
    }

    public void testNamespace() throws Exception {
        writer.writeStartElement(createName("foo")).writeNamespace("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);

        verify(stream).writeNamespace("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
    }

    public void testDefaultNamespace() throws Exception {
        String namespace = "http://www.example.com/ns/mynamespace";

        writer.writeStartElement(createName("foo")).writeDefaultNamespace(namespace);

        verify(stream).writeDefaultNamespace(namespace);
    }

    public void testComment() throws Exception {
        writer.writeComment("Some comment");
        writer.writeStartElement(createName("foo"));

        verify(stream, times(1)).writeStartDocument(ENCODING, VERSION);
        verify(stream).writeComment("Some comment");
    }

    public void testCData() throws Exception {
        String cdata = "Some cdata stuff here < & \" ";
        writer.writeCData(cdata);
        writer.writeStartElement(createName("foo"));

        verify(stream).writeStartDocument(ENCODING, VERSION);
        verify(stream).writeCData(cdata);
    }

    public void testFinish() throws Exception {
        writer.writeStartElement(createName("foo")).finish();

        verify(stream).flush();
        verify(stream).close();
    }

    public void testFinishNoElements() throws Exception {
        writer.writeComment("comment");
        writer.finish();
        verify(stream).flush();
        verify(stream).close();
    }

    public void testEndElementOnFinish() throws Exception {
        writer.writeStartElement(createName("foo")).writeStartElement(createName("bar"))
                .writeElement(createName("foobar"), "stuff");

        // Even though we never explicitly call endElement for the first two start elements, finish will do that for us.
        writer.finish();

        verify(stream).writeStartElement("foo");
        verify(stream).writeStartElement("bar");
        verify(stream).writeStartElement("foobar");
        verify(stream).writeCharacters("stuff");
        verify(stream, times(3)).writeEndElement();
    }

    public void testFormatter() throws Exception {
        InOrder order = inOrder(formatter, stream);

        // Start element
        writer.writeStartElement(createName("foo"));
        verifyFormatter(order, XMLStreamConstants.START_DOCUMENT, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                // first start element also writes the document
                order.verify(stream).writeStartDocument(ENCODING, VERSION);
            }
        });
        verifyFormatter(order, XMLStreamConstants.START_ELEMENT, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeStartElement("foo");
            }
        });

        // Namespace
        writer.writeNamespace("prefix", "uri");
        verifyFormatter(order, XMLStreamConstants.NAMESPACE, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeNamespace("prefix", "uri");
            }
        });

        // Default namespace
        writer.writeDefaultNamespace("uri");
        verifyFormatter(order, XMLStreamConstants.NAMESPACE, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeDefaultNamespace("uri");
            }
        });

        // Attribute
        writer.writeAttribute("name", "value");
        verifyFormatter(order, XMLStreamConstants.ATTRIBUTE, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeAttribute("name", "value");
            }
        });

        // Comment
        writer.writeComment("comment");
        verifyFormatter(order, XMLStreamConstants.COMMENT, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeComment("comment");
            }
        });

        // CData
        writer.writeCData("cdata");
        verifyFormatter(order, XMLStreamConstants.CDATA, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeCData("cdata");
            }
        });

        // Content
        writer.writeContent("content");
        verifyFormatter(order, XMLStreamConstants.CHARACTERS, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeCharacters("content");
            }
        });

        // End element
        writer.writeEndElement();
        verifyFormatter(order, XMLStreamConstants.END_ELEMENT, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeEndElement();
            }
        });

        // End document
        writer.finish();
        verifyFormatter(order, XMLStreamConstants.END_DOCUMENT, new VerifyClosure() {
            public void verify(InOrder order) throws Exception {
                order.verify(stream).writeEndDocument();
            }
        });
    }

    protected void verifyFormatter(InOrder order, int event, VerifyClosure closure) throws Exception {
        order.verify(formatter).before(stream, event);
        closure.verify(order);
        order.verify(formatter).after(stream, event);
    }

    private interface VerifyClosure {
        void verify(InOrder order) throws Exception;
    }
}
