/*
 * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Denise Smith -  November, 2009
package org.eclipse.persistence.testing.jaxb.typemappinginfo;

import java.awt.Image;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import jakarta.activation.DataHandler;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.SchemaOutputResolver;
import jakarta.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.JAXBMarshaller;
import org.eclipse.persistence.jaxb.JAXBUnmarshaller;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.eclipse.persistence.jaxb.TypeMappingInfo;

import org.eclipse.persistence.oxm.NamespaceResolver;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.XMLRoot;
import org.eclipse.persistence.testing.jaxb.JAXBXMLComparer;
import org.eclipse.persistence.testing.oxm.OXTestCase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import static org.eclipse.persistence.testing.jaxb.JAXBTestCases.ECLIPSELINK_OXM_XSD;

public abstract class TypeMappingInfoTestCases extends OXTestCase {
    protected JAXBContext jaxbContext;
    protected Marshaller jaxbMarshaller;
    protected Unmarshaller jaxbUnmarshaller;
    public String resourceName;
    protected Document controlDocument;
    protected Document writeControlDocument;

    protected DocumentBuilder parser;
    protected Source bindingsFileXSDSource;
    protected String controlDocumentLocation;
    protected String writeControlDocumentLocation;

    protected TypeMappingInfo[] typeMappingInfos;

    public TypeMappingInfoTestCases(String name) throws Exception {
        super(name);
    }

    public void setUp() throws Exception {
        setupParser();
        setupControlDocs();

        InputStream bindingsFileXSDInputStream = getClass().getClassLoader().getResourceAsStream(ECLIPSELINK_OXM_XSD);
        if(bindingsFileXSDInputStream == null){
            bindingsFileXSDInputStream = getClass().getClassLoader().getResourceAsStream("org/eclipse/persistence/jaxb/" + ECLIPSELINK_OXM_XSD);
        }
        if(bindingsFileXSDInputStream == null){
            fail("ERROR LOADING " + ECLIPSELINK_OXM_XSD);
        }
        bindingsFileXSDSource = new StreamSource(bindingsFileXSDInputStream);
    }

    public void tearDown() throws Exception{
        super.tearDown();
        jaxbContext = null;
        jaxbMarshaller = null;
        jaxbUnmarshaller = null;
    }


    public void setTypeMappingInfos(TypeMappingInfo[] newTypes) throws Exception {
        typeMappingInfos = newTypes;

        Map props = getProperties();
        if(props != null){
            Map overrides = (Map) props.get(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY);
            if(overrides != null){
                Iterator valuesIter = overrides.values().iterator();
                while(valuesIter.hasNext()){
                    Source theSource = (Source) valuesIter.next();
                    validateBindingsFileAgainstSchema(theSource);
                }
            }
        }

        jaxbContext  = new org.eclipse.persistence.jaxb.JAXBContextFactory().createContext(newTypes, props, Thread.currentThread().getContextClassLoader());
        jaxbMarshaller = jaxbContext.createMarshaller();
        jaxbUnmarshaller = jaxbContext.createUnmarshaller();
     }
    public TypeMappingInfo getTypeMappingInfo(){
        return typeMappingInfos[0];
    }

    protected Map getProperties() {
        return null;
    }

    public void testXMLToObjectFromXMLEventReaderWithTypeMappingInfo() throws Exception {
        if(null != XML_INPUT_FACTORY) {
            InputStream instream = ClassLoader.getSystemResourceAsStream(resourceName);
            javax.xml.stream.XMLEventReader reader = XML_INPUT_FACTORY.createXMLEventReader(instream);
            Object obj = ((org.eclipse.persistence.jaxb.JAXBUnmarshaller)jaxbUnmarshaller).unmarshal(reader, getTypeMappingInfo());

            Object controlObj = getReadControlObject();
            xmlToObjectTest(obj, controlObj);
        }
    }

    public void testXMLToObjectFromXMLStreamReaderWithTypeMappingInfo() throws Exception {
        if(null != XML_INPUT_FACTORY) {
            InputStream instream = ClassLoader.getSystemResourceAsStream(resourceName);
            XMLStreamReader xmlStreamReader = XML_INPUT_FACTORY.createXMLStreamReader(instream);
            Object testObject = ((JAXBUnmarshaller)jaxbUnmarshaller).unmarshal(xmlStreamReader, getTypeMappingInfo());
            instream.close();

            Object controlObj = getReadControlObject();
            xmlToObjectTest(testObject, controlObj);
        }
    }

    public void testXMLToObjectFromSourceWithTypeMappingInfoXML() throws Exception {
        InputStream instream = ClassLoader.getSystemResourceAsStream(resourceName);
        StreamSource ss = new StreamSource(instream);
        Object testObject = ((JAXBUnmarshaller)jaxbUnmarshaller).unmarshal(ss, getTypeMappingInfo());
        instream.close();

        Object controlObj = getReadControlObject();
        xmlToObjectTest(testObject, controlObj);
    }

    public void testObjectToResultWithTypeMappingInfoXML() throws Exception {

        Object objectToWrite = getWriteControlObject();
        XMLDescriptor desc = null;
        if (objectToWrite instanceof XMLRoot) {
            desc = (XMLDescriptor)((org.eclipse.persistence.jaxb.JAXBContext)jaxbContext).getXMLContext().getSession(0).getProject().getDescriptor(((XMLRoot)objectToWrite).getObject().getClass());
        } else {
            desc = (XMLDescriptor)((org.eclipse.persistence.jaxb.JAXBContext)jaxbContext).getXMLContext().getSession(0).getProject().getDescriptor(objectToWrite.getClass());
        }

        int sizeBefore = getNamespaceResolverSize(desc);
        StringWriter stringWriter = new StringWriter();
        StreamResult result = new StreamResult(stringWriter);
        jaxbMarshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/xml");

        ((JAXBMarshaller)jaxbMarshaller).marshal(objectToWrite, result, getTypeMappingInfo());

        int sizeAfter = getNamespaceResolverSize(desc);

        assertEquals(sizeBefore, sizeAfter);
        StringReader reader = new StringReader(stringWriter.toString());
        InputSource inputSource = new InputSource(reader);
        Document testDocument = parser.parse(inputSource);
        stringWriter.close();
        reader.close();
        objectToXMLDocumentTest(testDocument);
    }

    public void testObjectToXMLStreamWriterWithTypeMappingInfo() throws Exception {
        if(XML_OUTPUT_FACTORY != null) {
            StringWriter writer = new StringWriter();

            XMLOutputFactory factory = XMLOutputFactory.newInstance();
            factory.setProperty(factory.IS_REPAIRING_NAMESPACES, new Boolean(false));
            XMLStreamWriter streamWriter= factory.createXMLStreamWriter(writer);

            Object objectToWrite = getWriteControlObject();
            XMLDescriptor desc = null;
            if (objectToWrite instanceof XMLRoot) {

                desc = (XMLDescriptor)((org.eclipse.persistence.jaxb.JAXBContext)jaxbContext).getXMLContext().getSession(0).getProject().getDescriptor(((XMLRoot)objectToWrite).getObject().getClass());
            } else {
                desc = (XMLDescriptor)((org.eclipse.persistence.jaxb.JAXBContext)jaxbContext).getXMLContext().getSession(0).getProject().getDescriptor(objectToWrite.getClass());
            }

            int sizeBefore = getNamespaceResolverSize(desc);
            ((JAXBMarshaller)jaxbMarshaller).marshal(objectToWrite, streamWriter, getTypeMappingInfo());

            streamWriter.flush();
            int sizeAfter = getNamespaceResolverSize(desc);

            assertEquals(sizeBefore, sizeAfter);
            StringReader reader = new StringReader(writer.toString());
            InputSource inputSource = new InputSource(reader);
            Document testDocument = parser.parse(inputSource);
            writer.close();
            reader.close();
            objectToXMLDocumentTest(testDocument);
        }
    }

    public void testObjectToXMLEventWriterWithTypeMappingInfo() throws Exception {
        if(XML_OUTPUT_FACTORY != null) {
            StringWriter writer = new StringWriter();

            XMLOutputFactory factory = XMLOutputFactory.newInstance();
            factory.setProperty(factory.IS_REPAIRING_NAMESPACES, new Boolean(false));
            XMLEventWriter eventWriter= factory.createXMLEventWriter(writer);

            Object objectToWrite = getWriteControlObject();
            XMLDescriptor desc = null;
            if (objectToWrite instanceof XMLRoot) {
                desc = (XMLDescriptor)((org.eclipse.persistence.jaxb.JAXBContext)jaxbContext).getXMLContext().getSession(0).getProject().getDescriptor(((XMLRoot)objectToWrite).getObject().getClass());
            } else {
                desc = (XMLDescriptor)((org.eclipse.persistence.jaxb.JAXBContext)jaxbContext).getXMLContext().getSession(0).getProject().getDescriptor(objectToWrite.getClass());
            }

            int sizeBefore = getNamespaceResolverSize(desc);
            ((JAXBMarshaller)jaxbMarshaller).marshal(objectToWrite, eventWriter, getTypeMappingInfo());

            eventWriter.flush();
            int sizeAfter = getNamespaceResolverSize(desc);

            assertEquals(sizeBefore, sizeAfter);
            StringReader reader = new StringReader(writer.toString());
            InputSource inputSource = new InputSource(reader);
            Document testDocument = parser.parse(inputSource);
            writer.close();
            reader.close();
            objectToXMLDocumentTest(testDocument);
        }
    }

    public void xmlToObjectTest(Object testObject, Object controlObject) throws Exception {
        log("\n**xmlToObjectTest**");
        log("Expected:");
        log(controlObject.toString());
        log("Actual:");
        log(testObject.toString());

        JAXBElement controlObj = (JAXBElement)controlObject;
        JAXBElement testObj = (JAXBElement)testObject;
        compareJAXBElementObjects(controlObj, testObj);
    }

    public abstract Map<String, InputStream> getControlSchemaFiles();

    public void testSchemaGen() throws Exception {
        testSchemaGen(getControlSchemaFiles());
    }

     protected void compareValues(Object controlValue, Object testValue){
         if(controlValue instanceof Node && testValue instanceof Node) {
             assertXMLIdentical(((Node)controlValue).getOwnerDocument(), ((Node)testValue).getOwnerDocument());
         } else if(controlValue instanceof DataHandler && testValue instanceof DataHandler){
             compareDataHandlers((DataHandler)controlValue, (DataHandler)testValue);
         } else if(controlValue instanceof Image && testValue instanceof Image) {
             compareImages((Image)controlValue, (Image) testValue);
         } else if (controlValue instanceof Byte[] && testValue instanceof Byte[]){
                compareByteArrays((Byte[])controlValue, (Byte[])testValue);
         } else if (controlValue instanceof byte[] && testValue instanceof byte[]){
                 compareByteArrays((byte[])controlValue, (byte[])testValue);
         } else {
             assertEquals(controlValue, testValue);
         }
     }

     private void compareDataHandlers(DataHandler controlValue, DataHandler testValue){
         assertEquals(controlValue.getContentType(), testValue.getContentType());
         try{
             assertEquals(controlValue.getContent(), testValue.getContent());
         }catch(Exception e){
              e.printStackTrace();
              fail();
          }
     }

     private void compareImages(Image controlImage, Image testImage) {
         assertEquals(controlImage.getWidth(null), testImage.getWidth(null));
         assertEquals(controlImage.getHeight(null), testImage.getHeight(null));
     }
       protected void setupParser() {
            try {
                DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
                builderFactory.setNamespaceAware(true);
                builderFactory.setIgnoringElementContentWhitespace(true);
                parser = builderFactory.newDocumentBuilder();
            } catch (Exception e) {
                e.printStackTrace();
                fail("An exception occurred during setup");
            }
        }

       public void setupControlDocs() throws Exception{
            if(this.controlDocumentLocation != null) {
                InputStream inputStream = ClassLoader.getSystemResourceAsStream(controlDocumentLocation);
                resourceName = controlDocumentLocation;
                controlDocument = parser.parse(inputStream);
                removeEmptyTextNodes(controlDocument);

                inputStream.close();
            }

        if(this.writeControlDocumentLocation != null) {
                InputStream inputStream = ClassLoader.getSystemResourceAsStream(writeControlDocumentLocation);
                writeControlDocument = parser.parse(inputStream);
                removeEmptyTextNodes(writeControlDocument);

                inputStream.close();
            }
        }
       protected int getNamespaceResolverSize(XMLDescriptor desc){
           int size = -1;
            if (desc != null) {
                NamespaceResolver nr = desc.getNamespaceResolver();
                if (nr != null) {
                    size = nr.getNamespaces().size();
                }else{
                  size =0;
                }
            }
            return size;
        }

       protected Document getControlDocument() {
            return controlDocument;
        }

        /**
         * Override this function to implement different read/write control documents.
         * @return
         * @throws Exception
         */
        protected Document getWriteControlDocument() throws Exception {
            if(writeControlDocument != null){
                return writeControlDocument;
            }
            return getControlDocument();
        }

        protected void setControlDocument(String xmlResource) throws Exception {
            this.controlDocumentLocation = xmlResource;
        }

        /**
         * Provide an alternative write version of the control document when rountrip is not enabled.
         * If this function is not called and getWriteControlDocument() is not overridden then the write and read control documents are the same.
         * @param xmlResource
         * @throws Exception
         */
        protected void setWriteControlDocument(String xmlResource) throws Exception {
            writeControlDocumentLocation = xmlResource;
        }

        abstract protected Object getControlObject();
        /*
         * Returns the object to be used in a comparison on a read
         * This will typically be the same object used to write
         */
        public Object getReadControlObject() {
            return getControlObject();
        }

        /*
         * Returns the object to be written to XML which will be compared
         * to the control document.
         */
        public Object getWriteControlObject() {
            return getControlObject();
        }

        public void objectToXMLDocumentTest(Document testDocument) throws Exception {
            log("**objectToXMLDocumentTest**");
            log("Expected:");
            log(getWriteControlDocument());
            log("\nActual:");
            log(testDocument);
            assertXMLIdentical(getWriteControlDocument(), testDocument);
        }

        public void compareJAXBElementObjects(JAXBElement controlObj, JAXBElement testObj) {
            assertEquals(controlObj.getName().getLocalPart(), testObj.getName().getLocalPart());
            assertEquals(controlObj.getName().getNamespaceURI(), testObj.getName().getNamespaceURI());
            assertEquals(controlObj.getDeclaredType(), testObj.getDeclaredType());

            Object controlValue = controlObj.getValue();
            Object testValue = testObj.getValue();

            if(controlValue == null) {
                if(testValue == null){
                    return;
                }
                fail("Test value should have been null");
            }else{
                if(testValue == null){
                    fail("Test value should not have been null");
                }
            }

            if(controlValue.getClass() == ClassConstants.ABYTE && testValue.getClass() == ClassConstants.ABYTE ||
                controlValue.getClass() == ClassConstants.APBYTE && testValue.getClass() == ClassConstants.APBYTE){
                compareValues(controlValue, testValue);
            }else if(controlValue.getClass().isArray()){
                if(testValue.getClass().isArray()){
                    if(controlValue.getClass().getComponentType().isPrimitive()){
                        comparePrimitiveArrays(controlValue, testValue);
                    }else{
                        compareObjectArrays(controlValue, testValue);
                    }
                }else{
                    fail("Expected an array value but was an " + testValue.getClass().getName());
                }
            }
            else if (controlValue instanceof Collection){
                Collection controlCollection = (Collection)controlValue;
                Collection testCollection = (Collection)testValue;
                Iterator<Object> controlIter = controlCollection.iterator();
                Iterator<Object> testIter = testCollection.iterator();
                assertEquals(controlCollection.size(), testCollection.size());
                while(controlIter.hasNext()){
                    Object nextControl = controlIter.next();
                    Object nextTest = testIter.next();
                    compareValues(nextControl, nextTest);
                }
            }else{
                compareValues(controlValue, testValue);
            }
        }

        protected void comparePrimitiveArrays(Object controlValue, Object testValue) {
            if (controlValue == null && testValue == null) {
                // equal
                return;
            }

            assertEquals(controlValue.getClass(), testValue.getClass());

            assertNotNull(controlValue);
            assertNotNull(testValue);

            int controlLength = Array.getLength(controlValue);
            int testLength = Array.getLength(controlValue);
            assertEquals(controlLength, testLength);

            for (int i = 0; i < controlLength; i++) {
                Object controlObject = Array.get(controlValue, i);
                Object testObject = Array.get(testValue, i);
                assertEquals(controlObject, testObject);
            }
        }

        protected void  compareObjectArrays(Object controlValue, Object testValue){
            assertEquals(((Object[])controlValue).length,((Object[])testValue).length);
            for(int i=0; i<((Object[])controlValue).length-1; i++){
                assertEquals(((Object[])controlValue)[i], ((Object[])testValue)[i]);
            }
        }

        public void testSchemaGen(Map<String, InputStream> controlSchemas) throws Exception {
            MyStreamSchemaOutputResolver outputResolver = new MyStreamSchemaOutputResolver();
            jaxbContext.generateSchema(outputResolver);

            Map<String, Writer> generatedSchemas = outputResolver.schemaFiles;
            assertEquals(controlSchemas.size(), generatedSchemas.size());

            for(String next:controlSchemas.keySet()){
                InputStream nextControlValue = controlSchemas.get(next);

                Writer sw = generatedSchemas.get(next);
                InputSource nextGeneratedValue = new InputSource(new StringReader(sw.toString()));

                assertNotNull("Generated Schema not found.", nextGeneratedValue);

                Document control = parser.parse(nextControlValue);
                Document test = parser.parse(nextGeneratedValue);

                JAXBXMLComparer xmlComparer = new JAXBXMLComparer();
                boolean isEqual = xmlComparer.isSchemaEqual(control, test);
                if(!isEqual){
                    log("Expected Schema\n");
                    log(control);
                    log("ActualSchema\n");
                    log(test);
                }
                assertTrue("generated schema did not match control schema", isEqual);
            }
        }

    /**
     * Return an Element for a given xml-element snippet.
     *
     * @param xmlelement
     * @return
     * @throws Exception
     */
    protected Element getXmlElement(String xmlelement) throws Exception {
        StringReader str = new StringReader(xmlelement);
        InputSource is = new InputSource(str);
        try {
            return parser.parse(is).getDocumentElement();
        } catch (Exception e) {
            throw e;
        }
    }

    private boolean compareByteArrays(Byte[] first, Byte[] second){
        if(first.length != second.length){
            return false;
        }

        for(int i=0; i<first.length; i++){
            if (first[i] != second[i]){
                return false;
            }
        }
        return true;
    }

     private boolean compareByteArrays(byte[] first, byte[] second){
        if(first.length != second.length){
            return false;
        }

        for(int i=0; i<first.length; i++){
            if (first[i] != second[i]){
                return false;
            }
        }
        return true;
    }

    protected void validateBindingsFileAgainstSchema(Source src) {
         String result = null;
         SchemaFactory sFact = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
         Schema theSchema;
         try {
             theSchema = sFact.newSchema(bindingsFileXSDSource);
             Validator validator = theSchema.newValidator();

             validator.validate(src);
         } catch (Exception e) {
             e.printStackTrace();
             if (e.getMessage() == null) {
                 result = "An unknown exception occurred.";
             }
             result = e.getMessage();
         }
         assertTrue("Schema validation failed unxepectedly: " + result, result == null);
     }

    public static class MyStreamSchemaOutputResolver extends SchemaOutputResolver {
        // keep a list of processed schemas for the validation phase of the test(s)
        public Map<String, Writer> schemaFiles;

        public MyStreamSchemaOutputResolver() {
            schemaFiles = new HashMap<String, Writer>();
        }

        public Result createOutput(String namespaceURI, String suggestedFileName) throws IOException {
            //return new StreamResult(System.out);
            if (namespaceURI == null) {
                namespaceURI = "";
            }

            StringWriter sw = new StringWriter();
            schemaFiles.put(namespaceURI, sw);
            Result res = new StreamResult(sw);
            res.setSystemId(suggestedFileName);
            return res;
        }
    }
}





