/*
 * Copyright (c) eHealth
 */
package be.ehealth.technicalconnector.ws;

import be.ehealth.technicalconnector.config.ConfigFactory;
import be.ehealth.technicalconnector.config.ConfigValidator;
import be.ehealth.technicalconnector.exception.TechnicalConnectorException;
import be.ehealth.technicalconnector.handler.SchemaValidatorHandler;
import be.ehealth.technicalconnector.handler.domain.WsAddressingHeader;
import be.ehealth.technicalconnector.jaxb.AttachmentRoot;
import be.ehealth.technicalconnector.service.sts.security.Credential;
import be.ehealth.technicalconnector.service.sts.security.impl.KeyStoreCredential;
import be.ehealth.technicalconnector.utils.ByteArrayDatasource;
import be.ehealth.technicalconnector.ws.domain.GenericRequest;
import be.ehealth.technicalconnector.ws.domain.HandlerChain;
import be.ehealth.technicalconnector.ws.domain.HandlerPosition;
import be.ehealth.technicalconnector.ws.domain.TokenType;
import be.ehealth.technicalconnector.ws.feature.GenericFeature;
import be.ehealth.technicalconnector.ws.feature.SHA1Feature;
import be.ehealth.technicalconnector.ws.feature.SHA256Feature;
import be.ehealth.technicalconnector.ws.feature.XOPFeature;
import be.fgov.ehealth.technicalconnector.bootstrap.bcp.EndpointDistributor;
import be.fgov.ehealth.technicalconnector.bootstrap.bcp.domain.EndPointInformation;
import be.fgov.ehealth.technicalconnector.tests.junit.rule.HttpServerStubRule;
import be.fgov.ehealth.technicalconnector.tests.junit.rule.SessionRule;
import be.fgov.ehealth.technicalconnector.tests.server.EchoResponse;
import be.fgov.ehealth.technicalconnector.tests.server.EchoResponse.Inbound;
import org.apache.commons.lang3.StringUtils;
import org.junit.*;

import javax.activation.DataHandler;
import javax.xml.ws.soap.SOAPFaultException;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import static be.ehealth.technicalconnector.ws.ServiceFactory.getGenericWsSender;
import static be.fgov.ehealth.technicalconnector.tests.utils.XmlAsserter.assertXPath;
import static org.apache.commons.lang3.StringUtils.contains;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;

/**
 * @author eHealth Platform (EHP)
 */
public class GenericWsSenderTest {

    private static final String PAYLOAD = "body";

    @Rule
    public HttpServerStubRule server = new HttpServerStubRule();

    @ClassRule
    public static SessionRule rule = SessionRule.withInactiveSession().build();

    private Map<String, String> originalProperties = new HashMap<>();
    
    private ConfigValidator config = ConfigFactory.getConfigValidator();

    @Before
    public void before() {
        originalProperties.clear();

        backupProperty("default.rsa.digest.method.algorithm");
        backupProperty("default.rsa.signature.method.algorithm");
        backupProperty("connector.soaphandler.connection.request.timeout.duration");
        backupProperty("connector.soaphandler.connection.request.timeout.timeunit");
        backupProperty("connector.soaphandler.connection.connection.timeout.duration");
        backupProperty("connector.soaphandler.connection.connection.timeout.timeunit");
    }

    public void backupProperty(String property) {
        originalProperties.put(property, config.getProperty(property));
    }


    @After
    public void after() {
        ConfigValidator config = ConfigFactory.getConfigValidator();
        for (Map.Entry<String, String> entry : originalProperties.entrySet()) {
            config.setProperty(entry.getKey(), entry.getValue());
        }
        originalProperties.clear();
    }

    @Test
    public void validateXOP() throws Exception {
        AttachmentRoot content = new AttachmentRoot();
        content.setContentInline("contentInLine".getBytes());
        HandlerChain chain = new HandlerChain();
        chain.register(HandlerPosition.BEFORE, new SchemaValidatorHandler(SchemaValidatorHandler.VERIFY_OUTBOUND, "/be/ehealth/technicalconnector/jaxb/AttachmentRoot.xsd"));
        invoke(new GenericRequest().setPayload(content, new XOPFeature(5))
                .addHandlerChain(chain));
    }

    @Test
    public void inboundXOP() throws Exception {
        server.add("/full/mtom", "/stub/xop_with_special_contentid.xml");

        GenericRequest req = new GenericRequest()
                .setPayload(new AttachmentRoot())
                .setEndpoint(server.getContentUrl("/full/mtom"));

        AttachmentRoot actual = getGenericWsSender()
                .send(req)
                .asObject(AttachmentRoot.class);

        assertEquals("xop is my friend!", new String(actual.getContentInline()));

    }

    @Test
    public void inboundEmptyXOP() throws Exception {
        server.add("/full/mtom", "/stub/xop_empty_content.xml");

        GenericRequest req = new GenericRequest().setPayload(new AttachmentRoot()).setEndpoint(server.getContentUrl("/full/mtom"));

        AttachmentRoot actual = getGenericWsSender().send(req).asObject(AttachmentRoot.class);
        assertEquals("", new String(actual.getContentInline()));
    }

    @Test
    public void soapActionWSI() throws Exception {
        GenericRequest req = new GenericRequest().setPayload(new AttachmentRoot())
                .setSoapAction("soapAction:rocks");

        assertEquals("\"soapAction:rocks\"", invoke(req).getHeaders().get("SOAPAction"));
    }

    @Test
    public void soapActionNotWSI() throws Exception {
        GenericRequest req = new GenericRequest().setPayload(new AttachmentRoot()).setSoapAction("soapAction:rocks", false);

        assertEquals("soapAction:rocks", invoke(req).getHeaders().get("SOAPAction"));
    }

    @Test
    public void userAgent() throws Exception {
        GenericRequest req = new GenericRequest().setPayload(new AttachmentRoot()).setSoapAction("verify:user-agent");

        String result = invoke(req).getHeaders().get("User-Agent");

        Assert.assertTrue(result.startsWith("eHealthTechnical/"));
    }

    @Test
    public void sha1default() throws Exception {

        Credential credential = new KeyStoreCredential(rule.getSessionProperty("test.keystore.location"), rule.getSessionProperty("test.keystore.alias"), rule.getSessionProperty("test.keystore.password"));
        Assume.assumeThat(credential.getPublicKey().getAlgorithm(), equalTo("RSA"));

        ConfigValidator config = ConfigFactory.getConfigValidator();
        config.setProperty("default.rsa.digest.method.algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
        config.setProperty("default.rsa.signature.method.algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");

        Inbound result = invoke(new GenericRequest().setPayload(new AttachmentRoot()).setCredential(credential, TokenType.X509));
        String body = result.getFile().get(0).getValue();

        Map<String, String> namespace = new HashMap<String, String>();
        namespace.put("SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/");
        namespace.put("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        namespace.put("ds", "http://www.w3.org/2000/09/xmldsig#");

        assertXPath("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm", namespace, body);
        assertXPath("http://www.w3.org/2000/09/xmldsig#sha1", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:Reference[1]/ds:DigestMethod/@Algorithm", namespace, body);

    }

    @Test
    public void sha256default() throws Exception {

        Credential credential = new KeyStoreCredential(rule.getSessionProperty("test.keystore.location"), rule.getSessionProperty("test.keystore.alias"), rule.getSessionProperty("test.keystore.password"));
        Assume.assumeThat(credential.getPublicKey().getAlgorithm(), equalTo("EC"));

        ConfigValidator config = ConfigFactory.getConfigValidator();
        config.setProperty("default.ec.digest.method.algorithm", "http://www.w3.org/2001/04/xmlenc#sha256");
        config.setProperty("default.ec.signature.method.algorithm", "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256");

        Inbound result = invoke(new GenericRequest().setPayload(new AttachmentRoot()).setCredential(credential, TokenType.X509));
        String body = result.getFile().get(0).getValue();

        Map<String, String> namespace = new HashMap<String, String>();
        namespace.put("SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/");
        namespace.put("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        namespace.put("ds", "http://www.w3.org/2000/09/xmldsig#");

        assertXPath("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm", namespace, body);
        assertXPath("http://www.w3.org/2001/04/xmlenc#sha256", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:Reference[1]/ds:DigestMethod/@Algorithm", namespace, body);

    }


    @Test
    public void sha1feature() throws Exception {
        Credential credential = new KeyStoreCredential(rule.getSessionProperty("test.keystore.location"), rule.getSessionProperty("test.keystore.alias"), rule.getSessionProperty("test.keystore.password"));
        Assume.assumeThat(credential.getPublicKey().getAlgorithm(), equalTo("RSA"));

        Inbound result = invoke(new GenericRequest().setPayload(new AttachmentRoot(), new SHA1Feature()).setCredential(credential, TokenType.X509));
        String body = result.getFile().get(0).getValue();

        Map<String, String> namespace = new HashMap<String, String>();
        namespace.put("SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/");
        namespace.put("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        namespace.put("ds", "http://www.w3.org/2000/09/xmldsig#");

        assertXPath("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm", namespace, body);
        assertXPath("http://www.w3.org/2000/09/xmldsig#sha1", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:Reference[1]/ds:DigestMethod/@Algorithm", namespace, body);

    }

    @Test
    public void sha256feature() throws Exception {
        Credential credential = new KeyStoreCredential(rule.getSessionProperty("test.keystore.location"), rule.getSessionProperty("test.keystore.alias"), rule.getSessionProperty("test.keystore.password"));
        Assume.assumeThat(credential.getPublicKey().getAlgorithm(), equalTo("EC"));

        Inbound result = invoke(new GenericRequest().setPayload(new AttachmentRoot(), new SHA256Feature()).setCredential(credential, TokenType.X509));
        String body = result.getFile().get(0).getValue();

        Map<String, String> namespace = new HashMap<String, String>();
        namespace.put("SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/");
        namespace.put("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        namespace.put("ds", "http://www.w3.org/2000/09/xmldsig#");

        assertXPath("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm", namespace, body);
        assertXPath("http://www.w3.org/2001/04/xmlenc#sha256", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:Reference[1]/ds:DigestMethod/@Algorithm", namespace, body);

    }

    @Test
    public void outboundXOPThresholdExceed() throws Exception {
        AttachmentRoot content = new AttachmentRoot();
        content.setContentInline("contentInLine".getBytes());
        String payload = invoke(new GenericRequest().setPayload(content, new XOPFeature(10))).getFiles().get(PAYLOAD);

        Assert.assertTrue(contains(payload, "@ehealth.fgov.be\"/></ContentInline>"));
        Assert.assertTrue(contains(payload, "Content-Transfer-Encoding: binary"));
        Assert.assertTrue(contains(payload, "Content-ID: <"));
    }

    @Test
    public void outboundXOPThresholdNotExceed() throws Exception {
        AttachmentRoot content = new AttachmentRoot();
        content.setContentInline("contentInLine".getBytes());
        String payload = invoke(new GenericRequest().setPayload(content, new XOPFeature(25))).getFiles().get(PAYLOAD);
        Assert.assertTrue(contains(payload, "Y29udGVudEluTGluZQ=="));
    }

    @Test
    public void inline() throws Exception {
        AttachmentRoot content = new AttachmentRoot();
        content.setContentInline("contentInLine".getBytes());
        String payload = invoke(new GenericRequest().setPayload(content, (GenericFeature) null)).getFiles()
                .get(PAYLOAD);
        Assert.assertTrue(contains(payload, "Y29udGVudEluTGluZQ=="));
    }

    @Test
    public void swaref() throws Exception {
        AttachmentRoot content = new AttachmentRoot();
        content.setContentAsRef(new DataHandler(new ByteArrayDatasource("contentAsRef".getBytes())));
        String payload = invoke(new GenericRequest().setPayload(content, (GenericFeature) null)).getFiles()
                .get(PAYLOAD);
        Assert.assertTrue(contains(payload, "@ehealth.fgov.be</ContentAsRef>"));
    }

    @Test
    public void wsAddressing() throws Exception {
        AttachmentRoot content = new AttachmentRoot();
        String payload = invoke(new GenericRequest().setPayload(content)
                .setWSAddressing(new WsAddressingHeader(new URI("urn:be:fgov:ehealth:wsaddressing")))).getFiles()
                .get(PAYLOAD);
        Assert.assertTrue(contains(payload, "http://www.w3.org/2005/08/addressing"));
        Assert.assertTrue(contains(payload, "Action"));
        Assert.assertTrue(contains(payload, "urn:be:fgov:ehealth:wsaddressing"));
        Assert.assertTrue(contains(payload, "mustUnderstand=\"1\""));
    }

    @Test
    public void retry() throws Exception {
        String basePath = "/full/retry";
        server.add(basePath + "/step1", "/stub/genericws.sender.test.retry.step1.xml");
        server.add(basePath + "/step2", "/stub/genericws.sender.test.retry.step2.xml");

        String currentEndpoint = server.getContentUrl(basePath + "/step1");
        String alternativeEndpoint = server.getContentUrl(basePath + "/step2");

        EndPointInformation endPointInformation = new EndPointInformation();
        endPointInformation.register("generic:sender:test:retry", currentEndpoint, currentEndpoint, Arrays.asList(currentEndpoint, alternativeEndpoint), null);

        EndpointDistributor.getInstance().update(endPointInformation);

        GenericRequest request = new GenericRequest().setPayload("<dummy/>")
                .setEndpoint(currentEndpoint);

        String response = getGenericWsSender().send(request).asString();
        Assert.assertTrue(response.contains("<Whooot xmlns=\"urn:be:fgov:ehealth:connector:test\"/>"));


    }

    @Test
    public void timeout() throws Exception {
        config.setProperty("connector.soaphandler.connection.request.timeout.duration", "1");
        config.setProperty("connector.soaphandler.connection.request.timeout.timeunit", "SECONDS");
        config.setProperty("connector.soaphandler.connection.connection.timeout.duration", "1");
        config.setProperty("connector.soaphandler.connection.connection.timeout.timeunit", "SECONDS");

        String currentEndpoint = server.getTimeOutUrl(10);
        GenericRequest request = new GenericRequest()
                .setPayload("<dummy/>")
                .setEndpoint(currentEndpoint);
        
        TechnicalConnectorException ex = Assert.assertThrows(TechnicalConnectorException.class, () -> getGenericWsSender().send(request));
        Assert.assertTrue(StringUtils.contains(ex.getMessage(), "Read timed out"));
    }

    @Test(expected = SOAPFaultException.class)
    public void noRetry() throws Exception {
        String basePath = "/full/retry";
        server.add(basePath + "/step1", "/stub/genericws.sender.test.retry.step1.xml");

        GenericRequest request = new GenericRequest().setPayload("<dummy/>")
                .setEndpoint(server.getContentUrl(basePath + "/step1"));

        getGenericWsSender().send(request).asString();


    }

    private Inbound invoke(GenericRequest req) throws Exception {
        req.setEndpoint(server.getEchoUrl());
        return getGenericWsSender().send(req).asObject(EchoResponse.class).getInbound();
    }


}
