/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * 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.apache.cxf.jaxws.handler.soap;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.ws.Binding;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import org.apache.cxf.binding.soap.Soap11;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.SoapVersion;
import org.apache.cxf.binding.soap.SoapVersionFactory;
import org.apache.cxf.bus.managers.PhaseManagerImpl;
import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.InterceptorChain;
import org.apache.cxf.io.CachedOutputStream;
import org.apache.cxf.jaxws.handler.AbstractProtocolHandlerInterceptor;
import org.apache.cxf.jaxws.handler.HandlerChainInvoker;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.MessageImpl;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.staxutils.PartialXMLStreamReader;
import org.apache.cxf.staxutils.StaxUtils;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class SOAPHandlerInterceptorTest {

    @Before
    public void setUp() {
    }

    @After
    public void tearDown() {
    }

    // SAAJ tree is created from DOMXMLStreamWriter. Any changes to SOAPMessage should be streamed back to
    // outputStream
    @Test
    public void testChangeSOAPBodyOutBound() throws Exception {
        @SuppressWarnings("rawtypes")
        List<Handler> list = new ArrayList<>();
        list.add(new SOAPHandler<SOAPMessageContext>() {
            public boolean handleMessage(SOAPMessageContext smc) {
                Boolean outboundProperty = (Boolean)smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
                if (outboundProperty.booleanValue()) {
                    try {
                        smc.setMessage(prepareSOAPMessage("resources/greetMeRpcLitRespChanged.xml"));
                    } catch (Exception e) {
                        throw new Fault(e);
                    }
                }
                return true;
            }

            public boolean handleFault(SOAPMessageContext smc) {
                return true;
            }

            public Set<QName> getHeaders() {
                return null;
            }

            public void close(MessageContext messageContext) {
            }
        });
        HandlerChainInvoker invoker = new HandlerChainInvoker(list);

        Binding binding = mock(Binding.class);
        when(binding.getHandlerChain()).thenReturn(list);
        Exchange exchange = mock(Exchange.class);
        when(exchange.get(HandlerChainInvoker.class)).thenReturn(invoker);
        SoapMessage message = new SoapMessage(new MessageImpl());
        message.setExchange(exchange);
        // This is to set direction to outbound
        when(exchange.getOutMessage()).thenReturn(message);
        CachedStream originalEmptyOs = new CachedStream();

        XMLStreamWriter writer = StaxUtils.createXMLStreamWriter(originalEmptyOs);
        message.setContent(XMLStreamWriter.class, writer);

        InterceptorChain chain = new PhaseInterceptorChain((new PhaseManagerImpl()).getOutPhases());
        //Interceptors after SOAPHandlerInterceptor DOMXMLStreamWriter to write
        chain.add(new AbstractProtocolHandlerInterceptor<SoapMessage>(binding, Phase.MARSHAL) {

            public void handleMessage(SoapMessage message) throws Fault {
                try {
                    XMLStreamWriter writer = message.getContent(XMLStreamWriter.class);
                    SoapVersion soapVersion = Soap11.getInstance();
                    writer.setPrefix("soap", soapVersion.getNamespace());
                    writer.writeStartElement("soap",
                                          soapVersion.getEnvelope().getLocalPart(),
                                          soapVersion.getNamespace());
                    writer.writeNamespace("soap", soapVersion.getNamespace());
                    writer.writeEndElement();

                    writer.flush();
                } catch (Exception e) {
                    // do nothing
                }
            }

        });

        chain.add(new SOAPHandlerInterceptor(binding));
        message.setInterceptorChain(chain);

        chain.doIntercept(message);

        writer.flush();

        // Verify SOAPMessage
        SOAPMessage resultedMessage = message.getContent(SOAPMessage.class);
        assertNotNull(resultedMessage);
        SOAPBody bodyNew = resultedMessage.getSOAPBody();
        Iterator<?> itNew = bodyNew.getChildElements(new QName("http://apache.org/hello_world_rpclit",
                                                            "sendReceiveDataResponse"));
        SOAPBodyElement bodyElementNew = (SOAPBodyElement)itNew.next();
        Iterator<?> outIt = bodyElementNew
            .getChildElements(new QName("http://apache.org/hello_world_rpclit/types", "out"));
        Element outElement = (SOAPElement)outIt.next();
        assertNotNull(outElement);
        Element elem3Element =
            DOMUtils.findAllElementsByTagNameNS(outElement,
                                                "http://apache.org/hello_world_rpclit/types",
                                                "elem3").get(0);
        assertEquals("100", elem3Element.getTextContent());
    }

    @Test
    public void testChangeSOAPHeaderInBound() throws Exception {
        @SuppressWarnings("rawtypes")
        List<Handler> list = new ArrayList<>();
        list.add(new SOAPHandler<SOAPMessageContext>() {
            public boolean handleMessage(SOAPMessageContext smc) {
                try {
                    Boolean outboundProperty = (Boolean)smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
                    if (!outboundProperty.booleanValue()) {
                        // change mustUnderstand to false
                        SOAPMessage message = smc.getMessage();
                        SOAPHeader soapHeader = message.getSOAPHeader();
                        Element headerElementNew = (Element)soapHeader.getFirstChild();

                        SoapVersion soapVersion = Soap11.getInstance();
                        Attr attr =
                            headerElementNew.getOwnerDocument().createAttributeNS(soapVersion.getNamespace(),
                                                                                  "SOAP-ENV:mustUnderstand");
                        attr.setValue("false");
                        headerElementNew.setAttributeNodeNS(attr);
                    }
                } catch (Exception e) {
                    throw new Fault(e);
                }
                return true;
            }

            public boolean handleFault(SOAPMessageContext smc) {
                return true;
            }

            public Set<QName> getHeaders() {
                return null;
            }

            public void close(MessageContext messageContext) {
            }
        });
        HandlerChainInvoker invoker = new HandlerChainInvoker(list);

        Binding binding = mock(Binding.class);
        when(binding.getHandlerChain()).thenReturn(list);
        Exchange exchange = mock(Exchange.class);
        when(exchange.get(HandlerChainInvoker.class)).thenReturn(invoker);
        // This is to set direction to inbound
        when(exchange.getOutMessage()).thenReturn(null);

        SoapMessage message = new SoapMessage(new MessageImpl());
        message.setExchange(exchange);
        XMLStreamReader reader = preparemXMLStreamReader("resources/greetMeRpcLitReq.xml");
        message.setContent(XMLStreamReader.class, reader);
        Object[] headerInfo = prepareSOAPHeader();

        message.setContent(Node.class, headerInfo[0]);

        Node node = ((Element) headerInfo[1]).getFirstChild();

        message.getHeaders().add(new Header(new QName(node.getNamespaceURI(), node.getLocalName()), node));

        SOAPHandlerInterceptor li = new SOAPHandlerInterceptor(binding);
        li.handleMessage(message);

        // Verify SOAPMessage header
        SOAPMessage soapMessageNew = message.getContent(SOAPMessage.class);

        Element headerElementNew = DOMUtils.getFirstElement(soapMessageNew.getSOAPHeader());

        SoapVersion soapVersion = Soap11.getInstance();
        assertEquals("false", headerElementNew.getAttributeNS(soapVersion.getNamespace(), "mustUnderstand"));

        // Verify XMLStreamReader
        XMLStreamReader xmlReader = message.getContent(XMLStreamReader.class);
        QName qn = xmlReader.getName();
        assertEquals("sendReceiveData", qn.getLocalPart());

        // Verify Header Element
        Iterator<Header> iter = message.getHeaders().iterator();
        Element requiredHeader = null;
        while (iter.hasNext()) {
            Header localHdr = iter.next();
            if (localHdr.getObject() instanceof Element) {
                Element elem = (Element) localHdr.getObject();
                if ("http://apache.org/hello_world_rpclit/types".equals(elem.getNamespaceURI())
                        && "header1".equals(elem.getLocalName())) {
                    requiredHeader = (Element) localHdr.getObject();
                    break;
                }
            }
        }

        assertNotNull("Should have found header1", requiredHeader);
        assertEquals("false", requiredHeader.getAttributeNS(soapVersion.getNamespace(), "mustUnderstand"));
    }

    @Test
    public void testChangeSOAPHeaderOutBound() throws Exception {
        @SuppressWarnings("rawtypes")
        List<Handler> list = new ArrayList<>();
        list.add(new SOAPHandler<SOAPMessageContext>() {
            public boolean handleMessage(SOAPMessageContext smc) {
                try {
                    Boolean outboundProperty = (Boolean)smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
                    if (outboundProperty.booleanValue()) {
                        // change mustUnderstand to false
                        SOAPMessage message = smc.getMessage();

                        SOAPHeader soapHeader = message.getSOAPHeader();
                        Iterator<?> it = soapHeader.getChildElements(new QName(
                            "http://apache.org/hello_world_rpclit/types", "header1"));
                        SOAPHeaderElement headerElementNew = (SOAPHeaderElement)it.next();

                        SoapVersion soapVersion = Soap11.getInstance();
                        Attr attr =
                            headerElementNew.getOwnerDocument().createAttributeNS(soapVersion.getNamespace(),
                                                                                  "SOAP-ENV:mustUnderstand");
                        attr.setValue("false");
                        headerElementNew.setAttributeNodeNS(attr);
                    }
                } catch (Exception e) {
                    throw new Fault(e);
                }
                return true;
            }

            public boolean handleFault(SOAPMessageContext smc) {
                return true;
            }

            public Set<QName> getHeaders() {
                return null;
            }

            public void close(MessageContext messageContext) {
            }
        });
        HandlerChainInvoker invoker = new HandlerChainInvoker(list);

        Binding binding = mock(Binding.class);
        when(binding.getHandlerChain()).thenReturn(list);
        Exchange exchange = mock(Exchange.class);
        when(exchange.get(HandlerChainInvoker.class)).thenReturn(invoker);
        SoapMessage message = new SoapMessage(new MessageImpl());
        message.setExchange(exchange);
        // This is to set direction to outbound
        when(exchange.getOutMessage()).thenReturn(message);
        CachedStream originalEmptyOs = new CachedStream();
        message.setContent(OutputStream.class, originalEmptyOs);

        InterceptorChain chain = new PhaseInterceptorChain((new PhaseManagerImpl()).getOutPhases());
        //Interceptors after SOAPHandlerInterceptor DOMXMLStreamWriter to write
        chain.add(new AbstractProtocolHandlerInterceptor<SoapMessage>(binding, Phase.MARSHAL) {

            public void handleMessage(SoapMessage message) throws Fault {
                try {
                    XMLStreamWriter writer = message.getContent(XMLStreamWriter.class);
                    SoapVersion soapVersion = Soap11.getInstance();
                    writer.setPrefix("soap", soapVersion.getNamespace());
                    writer.writeStartElement("soap",
                                          soapVersion.getEnvelope().getLocalPart(),
                                          soapVersion.getNamespace());
                    writer.writeNamespace("soap", soapVersion.getNamespace());

                    Object[] headerInfo = prepareSOAPHeader();
                    StaxUtils.writeElement((Element) headerInfo[1], writer, true, false);

                    writer.writeEndElement();

                    writer.flush();
                } catch (Exception e) {
                    // do nothing
                }
            }

        });
        chain.add(new SOAPHandlerInterceptor(binding));
        message.setInterceptorChain(chain);

        chain.doIntercept(message);

        // Verify SOAPMessage header
        SOAPMessage soapMessageNew = message.getContent(SOAPMessage.class);

        SOAPHeader soapHeader = soapMessageNew.getSOAPHeader();
        Iterator<?> itNew = soapHeader
            .getChildElements(new QName("http://apache.org/hello_world_rpclit/types",
                "header1"));
        SOAPHeaderElement headerElementNew = (SOAPHeaderElement)itNew.next();
        SoapVersion soapVersion = Soap11.getInstance();
        assertEquals("false", headerElementNew.getAttributeNS(soapVersion.getNamespace(), "mustUnderstand"));
        originalEmptyOs.close();
    }

    @Test
    public void testGetSOAPMessageInBound() throws Exception {
        @SuppressWarnings("rawtypes")
        List<Handler> list = new ArrayList<>();
        list.add(new SOAPHandler<SOAPMessageContext>() {
            public boolean handleMessage(SOAPMessageContext smc) {
                try {
                    smc.getMessage();
                } catch (Exception e) {
                    throw new Fault(e);
                }
                return true;
            }

            public boolean handleFault(SOAPMessageContext smc) {
                return true;
            }

            public Set<QName> getHeaders() {
                return null;
            }

            public void close(MessageContext messageContext) {
            }
        });
        HandlerChainInvoker invoker = new HandlerChainInvoker(list);

        Binding binding = mock(Binding.class);
        Exchange exchange = mock(Exchange.class);
        when(binding.getHandlerChain()).thenReturn(list);
        when(exchange.get(HandlerChainInvoker.class)).thenReturn(invoker);
        // This is to set direction to inbound
        when(exchange.getOutMessage()).thenReturn(null);

        SoapMessage message = new SoapMessage(new MessageImpl());
        message.setExchange(exchange);

        XMLStreamReader reader = preparemXMLStreamReader("resources/greetMeRpcLitReq.xml");
        message.setContent(XMLStreamReader.class, reader);

        SOAPHandlerInterceptor li = new SOAPHandlerInterceptor(binding);
        li.handleMessage(message);

        // Verify SOAPMessage
        SOAPMessage soapMessageNew = message.getContent(SOAPMessage.class);
        SOAPBody bodyNew = soapMessageNew.getSOAPBody();
        Iterator<?> itNew = bodyNew.getChildElements();
        SOAPBodyElement bodyElementNew = (SOAPBodyElement)itNew.next();
        assertEquals("sendReceiveData", bodyElementNew.getLocalName());

        // Verify the XMLStreamReader
        XMLStreamReader xmlReader = message.getContent(XMLStreamReader.class);
        QName qn = xmlReader.getName();
        assertEquals("sendReceiveData", qn.getLocalPart());
    }

    @Test
    public void testGetUnderstoodHeadersReturnsNull() {
        @SuppressWarnings("rawtypes")
        List<Handler> list = new ArrayList<>();
        list.add(new SOAPHandler<SOAPMessageContext>() {
            public boolean handleMessage(SOAPMessageContext smc) {
                return true;
            }

            public boolean handleFault(SOAPMessageContext smc) {
                return true;
            }

            public Set<QName> getHeaders() {
                return null;
            }

            public void close(MessageContext messageContext) {
            }
        });
        HandlerChainInvoker invoker = new HandlerChainInvoker(list);

        Binding binding = mock(Binding.class);
        when(binding.getHandlerChain()).thenReturn(list);
        SoapMessage message = mock(SoapMessage.class);
        Exchange exchange = mock(Exchange.class);
        when(message.getExchange()).thenReturn(exchange);
        when(message.keySet()).thenReturn(new HashSet<>());
        when(exchange.get(HandlerChainInvoker.class)).thenReturn(invoker);

        SOAPHandlerInterceptor li = new SOAPHandlerInterceptor(binding);
        Set<QName> understood = li.getUnderstoodHeaders();
        assertNotNull(understood);
        assertTrue(understood.isEmpty());
    }

    private XMLStreamReader preparemXMLStreamReader(String resouceName) throws Exception {
        InputStream is = this.getClass().getResourceAsStream(resouceName);
        XMLStreamReader xmlReader = XMLInputFactory.newInstance().createXMLStreamReader(is);

        // skip until soap body
        if (xmlReader.nextTag() == XMLStreamConstants.START_ELEMENT) {
            String ns = xmlReader.getNamespaceURI();
            SoapVersion soapVersion = SoapVersionFactory.getInstance().getSoapVersion(ns);
            // message.setVersion(soapVersion);

            QName qn = xmlReader.getName();
            while (!qn.equals(soapVersion.getBody()) && !qn.equals(soapVersion.getHeader())) {
                while (xmlReader.nextTag() != XMLStreamConstants.START_ELEMENT) {
                    // nothing to do
                }
                qn = xmlReader.getName();
            }
            if (qn.equals(soapVersion.getHeader())) {
                XMLStreamReader filteredReader = new PartialXMLStreamReader(xmlReader, soapVersion.getBody());

                StaxUtils.read(filteredReader);
            }
            // advance just past body.
            xmlReader.next();

            while (xmlReader.isWhiteSpace()) {
                xmlReader.next();
            }
        }
        return xmlReader;
    }

    private Object[] prepareSOAPHeader() throws Exception {
        Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        SoapVersion soapVersion = Soap11.getInstance();
        Element envElement = doc.createElementNS(soapVersion.getEnvelope().getNamespaceURI(),
                                                 soapVersion.getEnvelope().getLocalPart());

        Element headerElement = doc.createElementNS(soapVersion.getNamespace(),
                                                    soapVersion.getHeader().getLocalPart());

        Element bodyElement = doc.createElementNS(soapVersion.getBody().getNamespaceURI(),
                                                  soapVersion.getBody().getLocalPart());

        Element childElement = doc.createElementNS("http://apache.org/hello_world_rpclit/types",
                                                   "ns2:header1");
        Attr attr =
            childElement.getOwnerDocument().createAttributeNS(soapVersion.getNamespace(),
                                                                  "SOAP-ENV:mustUnderstand");
        attr.setValue("true");
        childElement.setAttributeNodeNS(attr);

        headerElement.appendChild(childElement);
        envElement.appendChild(headerElement);
        envElement.appendChild(bodyElement);
        doc.appendChild(envElement);

        return new Object[] {doc, headerElement};
    }

    private SOAPMessage prepareSOAPMessage(String resouceName) throws Exception {
        InputStream is = this.getClass().getResourceAsStream(resouceName);
        MessageFactory factory = MessageFactory.newInstance();
        MimeHeaders mhs = null;
        return factory.createMessage(mhs, is);
    }


    private final class CachedStream extends CachedOutputStream {
        protected void doFlush() throws IOException {
            currentStream.flush();
        }

        protected void doClose() throws IOException {
        }

        protected void onWrite() throws IOException {
        }
    }

}