/*
 * 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.camel.coap;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;

import javax.crypto.KeyGenerator;

import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.support.jsse.KeyManagersParameters;
import org.apache.camel.support.jsse.KeyStoreParameters;
import org.apache.camel.support.jsse.SSLContextParameters;
import org.apache.camel.support.jsse.SSLContextServerParameters;
import org.apache.camel.support.jsse.TrustManagersParameters;
import org.apache.camel.test.AvailablePortFinder;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.scandium.dtls.pskstore.AdvancedPskStore;
import org.eclipse.californium.scandium.dtls.pskstore.AdvancedSinglePskStore;
import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier;
import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

abstract class CoAPComponentTLSTestBase extends CamelTestSupport {

    @RegisterExtension
    static AvailablePortFinder.Port PORT = AvailablePortFinder.find();
    @RegisterExtension
    static AvailablePortFinder.Port PORT2 = AvailablePortFinder.find();
    @RegisterExtension
    static AvailablePortFinder.Port PORT3 = AvailablePortFinder.find();
    @RegisterExtension
    static AvailablePortFinder.Port PORT4 = AvailablePortFinder.find();
    @RegisterExtension
    static AvailablePortFinder.Port PORT5 = AvailablePortFinder.find();
    @RegisterExtension
    static AvailablePortFinder.Port PORT6 = AvailablePortFinder.find();
    @RegisterExtension
    static AvailablePortFinder.Port PORT7 = AvailablePortFinder.find();
    @RegisterExtension
    static AvailablePortFinder.Port PORT8 = AvailablePortFinder.find();

    @ParameterizedTest
    @ValueSource(strings = { "direct:start", "direct:selfsigned", /*"direct:clientauth",*/ "direct:ciphersuites" })
    @DisplayName("Test calls with/without certificates")
    void testCall(String endpointUri) throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMinimumMessageCount(1);
        mock.expectedBodiesReceived("Hello Camel CoAP");
        mock.expectedHeaderReceived(CoAPConstants.CONTENT_TYPE,
                MediaTypeRegistry.toString(MediaTypeRegistry.APPLICATION_OCTET_STREAM));
        mock.expectedHeaderReceived(CoAPConstants.COAP_RESPONSE_CODE, CoAP.ResponseCode.CONTENT.toString());
        sendBodyAndHeader(endpointUri, "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
        MockEndpoint.assertIsSatisfied(context);
    }

    @Test
    void testSuccessfulCall() throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMinimumMessageCount(1);
        mock.expectedBodiesReceived("Hello Camel CoAP");
        mock.expectedHeaderReceived(CoAPConstants.CONTENT_TYPE,
                MediaTypeRegistry.toString(MediaTypeRegistry.APPLICATION_OCTET_STREAM));
        mock.expectedHeaderReceived(CoAPConstants.COAP_RESPONSE_CODE, CoAP.ResponseCode.CONTENT.toString());
        sendBodyAndHeader("direct:start", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
        MockEndpoint.assertIsSatisfied(context);
    }

    @ParameterizedTest
    @ValueSource(strings = { "direct:notruststore", "direct:failedtrust", "direct:failedclientauth" })
    @DisplayName("Tests different types of trust stores")
    void testTrustStores(String endpointUri) throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMessageCount(0);
        sendBodyAndHeader(endpointUri, "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
        MockEndpoint.assertIsSatisfied(context);
    }

    @Test
    void testRawPublicKey() throws Exception {
        if (isRPKSupported()) {
            MockEndpoint mock = getMockEndpoint("mock:result");
            mock.expectedMinimumMessageCount(1);
            mock.expectedBodiesReceived("Hello Camel CoAP");
            mock.expectedHeaderReceived(CoAPConstants.CONTENT_TYPE,
                    MediaTypeRegistry.toString(MediaTypeRegistry.APPLICATION_OCTET_STREAM));
            mock.expectedHeaderReceived(CoAPConstants.COAP_RESPONSE_CODE, CoAP.ResponseCode.CONTENT.toString());
            sendBodyAndHeader("direct:rpk", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
            MockEndpoint.assertIsSatisfied(context);
        }
    }

    @Test
    void testRawPublicKeyNoTruststore() throws Exception {
        if (isRPKSupported()) {
            MockEndpoint mock = getMockEndpoint("mock:result");
            mock.expectedMessageCount(0);
            sendBodyAndHeader("direct:rpknotruststore", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
            MockEndpoint.assertIsSatisfied(context);
        }
    }

    @Test
    void testRawPublicKeyFailedTrust() throws Exception {
        if (isRPKSupported()) {
            MockEndpoint mock = getMockEndpoint("mock:result");
            mock.expectedMessageCount(0);
            sendBodyAndHeader("direct:rpkfailedtrust", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
            MockEndpoint.assertIsSatisfied(context);
        }
    }

    @Test
    void testRawPublicKeyClientAuth() throws Exception {
        if (isRPKSupported()) {
            MockEndpoint mock = getMockEndpoint("mock:result");
            mock.expectedMinimumMessageCount(1);
            mock.expectedBodiesReceived("Hello Camel CoAP");
            mock.expectedHeaderReceived(CoAPConstants.CONTENT_TYPE,
                    MediaTypeRegistry.toString(MediaTypeRegistry.APPLICATION_OCTET_STREAM));
            mock.expectedHeaderReceived(CoAPConstants.COAP_RESPONSE_CODE, CoAP.ResponseCode.CONTENT.toString());
            sendBodyAndHeader("direct:rpkclientauth", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
            MockEndpoint.assertIsSatisfied(context);
        }
    }

    @Test
    void testPreSharedKey() throws Exception {
        if (isPSKSupported()) {
            MockEndpoint mock = getMockEndpoint("mock:result");
            mock.expectedMinimumMessageCount(1);
            mock.expectedBodiesReceived("Hello Camel CoAP");
            mock.expectedHeaderReceived(CoAPConstants.CONTENT_TYPE,
                    MediaTypeRegistry.toString(MediaTypeRegistry.APPLICATION_OCTET_STREAM));
            mock.expectedHeaderReceived(CoAPConstants.COAP_RESPONSE_CODE, CoAP.ResponseCode.CONTENT.toString());
            sendBodyAndHeader("direct:psk", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
            MockEndpoint.assertIsSatisfied(context);
        }
    }

    @Test
    void testPreSharedKeyCipherSuite() throws Exception {
        if (isPSKSupported()) {
            MockEndpoint mock = getMockEndpoint("mock:result");
            mock.expectedMinimumMessageCount(1);
            mock.expectedBodiesReceived("Hello Camel CoAP");
            mock.expectedHeaderReceived(CoAPConstants.CONTENT_TYPE,
                    MediaTypeRegistry.toString(MediaTypeRegistry.APPLICATION_OCTET_STREAM));
            mock.expectedHeaderReceived(CoAPConstants.COAP_RESPONSE_CODE, CoAP.ResponseCode.CONTENT.toString());
            sendBodyAndHeader("direct:pskciphersuite", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
            MockEndpoint.assertIsSatisfied(context);
        }
    }

    @Test
    void testPreSharedKeyX509() throws Exception {
        if (isPSKSupported()) {
            MockEndpoint mock = getMockEndpoint("mock:result");
            mock.expectedMinimumMessageCount(1);
            mock.expectedBodiesReceived("Hello Camel CoAP");
            mock.expectedHeaderReceived(CoAPConstants.CONTENT_TYPE,
                    MediaTypeRegistry.toString(MediaTypeRegistry.APPLICATION_OCTET_STREAM));
            mock.expectedHeaderReceived(CoAPConstants.COAP_RESPONSE_CODE, CoAP.ResponseCode.CONTENT.toString());
            sendBodyAndHeader("direct:pskx509", "Camel CoAP", CoAPConstants.COAP_METHOD, "POST");
            MockEndpoint.assertIsSatisfied(context);
        }
    }

    protected abstract String getProtocol();

    protected abstract boolean isPSKSupported();

    protected abstract boolean isRPKSupported();

    @Override
    protected RouteBuilder createRouteBuilder() throws Exception {

        registerTLSConfiguration(context);

        return new RouteBuilder() {
            @Override
            public void configure() {

                fromF(getProtocol() + "://localhost:%d/TestResource?sslContextParameters=#serviceSSLContextParameters",
                        PORT.getPort())
                        .transform(body().prepend("Hello "));

                fromF(getProtocol()
                      + "://localhost:%d/TestResource?alias=selfsigned&sslContextParameters=#selfSignedServiceSSLContextParameters",
                        PORT2.getPort())
                        .transform(body().prepend("Hello "));

                fromF(getProtocol()
                      + "://localhost:%d/TestResource?sslContextParameters=#serviceSSLContextParameters&cipherSuites=TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
                        PORT4.getPort())
                        .transform(body().prepend("Hello "));

                from("direct:start")
                        .toF(getProtocol() + "://localhost:%d/TestResource?sslContextParameters=#clientSSLContextParameters",
                                PORT.getPort())
                        .to("mock:result");

                from("direct:notruststore").toF(getProtocol() + "://localhost:%d/TestResource", PORT.getPort())
                        .to("mock:result");

                from("direct:failedtrust")
                        .toF(getProtocol() + "://localhost:%d/TestResource?sslContextParameters=#clientSSLContextParameters2",
                                PORT.getPort())
                        .to("mock:result");

                from("direct:selfsigned")
                        .toF(getProtocol()
                             + "://localhost:%d/TestResource?sslContextParameters=#selfSignedClientSSLContextParameters",
                                PORT2.getPort())
                        .to("mock:result");

                from("direct:clientauth")
                        .toF(getProtocol()
                             + "://localhost:%d/TestResource?sslContextParameters=#clientAuthClientSSLContextParameters",
                                PORT3.getPort())
                        .to("mock:result");

                from("direct:failedclientauth")
                        .toF(getProtocol()
                             + "://localhost:%d/TestResource?sslContextParameters=#clientAuthClientSSLContextParameters2",
                                PORT3.getPort())
                        .to("mock:result");

                from("direct:ciphersuites")
                        .toF(getProtocol()
                             + "://localhost:%d/TestResource?sslContextParameters=#clientSSLContextParameters&cipherSuites=TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
                                PORT4.getPort())
                        .to("mock:result");

                if (isRPKSupported()) {
                    fromF(getProtocol() + "://localhost:%d/TestResource?privateKey=#privateKey&publicKey=#publicKey",
                            PORT5.getPort())
                            .transform(body().prepend("Hello "));

                    fromF(getProtocol()
                          + "://localhost:%d/TestResource?privateKey=#privateKey&publicKey=#publicKey&clientAuthentication=NEEDED&advancedCertificateVerifier=#advancedCertificateVerifier",
                            PORT6.getPort()).transform(body().prepend("Hello "));

                    from("direct:rpk")
                            .toF(getProtocol()
                                 + "://localhost:%d/TestResource?advancedCertificateVerifier=#advancedCertificateVerifier",
                                    PORT5.getPort())
                            .to("mock:result");

                    from("direct:rpknotruststore").toF(getProtocol() + "://localhost:%d/TestResource", PORT5.getPort())
                            .to("mock:result");

                    //                    from("direct:rpkfailedtrust")
                    //                            .toF(getProtocol()
                    //                                 + "://localhost:%d/TestResource?advancedCertificateVerifier=#failedAdvancedCertificateVerifier",
                    //                                    PORT5)
                    //                            .to("mock:result");

                    from("direct:rpkclientauth")
                            .toF(getProtocol()
                                 + "://localhost:%d/TestResource?advancedCertificateVerifier=#advancedCertificateVerifier&privateKey=#privateKey&publicKey=#publicKey",
                                    PORT6.getPort())
                            .to("mock:result");
                }

                if (isPSKSupported()) {
                    fromF(getProtocol() + "://localhost:%d/TestResource?advancedPskStore=#advancedPskStore", PORT7.getPort())
                            .transform(body().prepend("Hello "));

                    fromF(getProtocol()
                          + "://localhost:%d/TestResource?sslContextParameters=#serviceSSLContextParameters&advancedPskStore=#advancedPskStore",
                            PORT8.getPort())
                            .transform(body().prepend("Hello "));

                    from("direct:psk")
                            .toF(getProtocol() + "://localhost:%d/TestResource?advancedPskStore=#advancedPskStore",
                                    PORT7.getPort())
                            .to("mock:result");

                    from("direct:pskciphersuite")
                            .toF(getProtocol()
                                 + "://localhost:%d/TestResource?advancedPskStore=#advancedPskStore&cipherSuites=TLS_PSK_WITH_AES_128_GCM_SHA256",
                                    PORT7.getPort())
                            .to("mock:result");

                    from("direct:pskx509")
                            .toF(getProtocol()
                                 + "://localhost:%d/TestResource?advancedPskStore=#advancedPskStore&sslContextParameters=#clientSSLContextParameters",
                                    PORT8.getPort())
                            .to("mock:result");
                }

            }
        };
    }

    private void registerTLSConfiguration(CamelContext context) throws GeneralSecurityException, IOException {
        KeyStoreParameters serviceKeystoreParameters = new KeyStoreParameters();
        serviceKeystoreParameters.setCamelContext(context);
        serviceKeystoreParameters.setResource("service.jks");
        serviceKeystoreParameters.setPassword("security");

        KeyStoreParameters selfSignedKeyStoreParameters = new KeyStoreParameters();
        selfSignedKeyStoreParameters.setCamelContext(context);
        selfSignedKeyStoreParameters.setResource("selfsigned.jks");
        selfSignedKeyStoreParameters.setPassword("security");

        KeyStoreParameters clientKeystoreParameters = new KeyStoreParameters();
        clientKeystoreParameters.setCamelContext(context);
        clientKeystoreParameters.setResource("client.jks");
        clientKeystoreParameters.setPassword("security");

        KeyStoreParameters truststoreParameters = new KeyStoreParameters();
        truststoreParameters.setCamelContext(context);
        truststoreParameters.setResource("truststore.jks");
        truststoreParameters.setPassword("storepass");

        KeyStoreParameters truststoreParameters2 = new KeyStoreParameters();
        truststoreParameters2.setCamelContext(context);
        truststoreParameters2.setResource("truststore2.jks");
        truststoreParameters2.setPassword("storepass");

        SSLContextParameters serviceSSLContextParameters = new SSLContextParameters();
        serviceSSLContextParameters.setCamelContext(context);
        KeyManagersParameters serviceSSLKeyManagers = new KeyManagersParameters();
        serviceSSLKeyManagers.setKeyPassword("security");
        serviceSSLKeyManagers.setKeyStore(serviceKeystoreParameters);
        serviceSSLContextParameters.setKeyManagers(serviceSSLKeyManagers);

        SSLContextParameters selfSignedServiceSSLContextParameters = new SSLContextParameters();
        selfSignedServiceSSLContextParameters.setCamelContext(context);
        KeyManagersParameters selfSignedServiceSSLKeyManagers = new KeyManagersParameters();
        selfSignedServiceSSLKeyManagers.setKeyPassword("security");
        selfSignedServiceSSLKeyManagers.setKeyStore(selfSignedKeyStoreParameters);
        selfSignedServiceSSLContextParameters.setKeyManagers(selfSignedServiceSSLKeyManagers);

        SSLContextParameters clientAuthServiceSSLContextParameters = new SSLContextParameters();
        clientAuthServiceSSLContextParameters.setCamelContext(context);
        KeyManagersParameters clientAuthServiceSSLKeyManagers = new KeyManagersParameters();
        clientAuthServiceSSLKeyManagers.setKeyPassword("security");
        clientAuthServiceSSLKeyManagers.setKeyStore(serviceKeystoreParameters);
        clientAuthServiceSSLContextParameters.setKeyManagers(clientAuthServiceSSLKeyManagers);
        TrustManagersParameters clientAuthServiceSSLTrustManagers = new TrustManagersParameters();
        clientAuthServiceSSLTrustManagers.setKeyStore(truststoreParameters);
        clientAuthServiceSSLContextParameters.setTrustManagers(clientAuthServiceSSLTrustManagers);
        SSLContextServerParameters clientAuthSSLContextServerParameters = new SSLContextServerParameters();
        clientAuthSSLContextServerParameters.setClientAuthentication("REQUIRE");
        clientAuthServiceSSLContextParameters.setServerParameters(clientAuthSSLContextServerParameters);

        SSLContextParameters clientSSLContextParameters = new SSLContextParameters();
        clientSSLContextParameters.setCamelContext(context);
        TrustManagersParameters clientSSLTrustManagers = new TrustManagersParameters();
        clientSSLTrustManagers.setKeyStore(truststoreParameters);
        clientSSLContextParameters.setTrustManagers(clientSSLTrustManagers);

        SSLContextParameters clientSSLContextParameters2 = new SSLContextParameters();
        clientSSLContextParameters2.setCamelContext(context);
        TrustManagersParameters clientSSLTrustManagers2 = new TrustManagersParameters();
        clientSSLTrustManagers2.setKeyStore(truststoreParameters2);
        clientSSLContextParameters2.setTrustManagers(clientSSLTrustManagers2);

        SSLContextParameters clientAuthClientSSLContextParameters = new SSLContextParameters();
        clientAuthClientSSLContextParameters.setCamelContext(context);
        TrustManagersParameters clientAuthClientSSLTrustManagers = new TrustManagersParameters();
        clientAuthClientSSLTrustManagers.setKeyStore(truststoreParameters);
        clientAuthClientSSLContextParameters.setTrustManagers(clientAuthClientSSLTrustManagers);
        KeyManagersParameters clientAuthClientSSLKeyManagers = new KeyManagersParameters();
        clientAuthClientSSLKeyManagers.setKeyPassword("security");
        clientAuthClientSSLKeyManagers.setKeyStore(clientKeystoreParameters);
        clientAuthClientSSLContextParameters.setKeyManagers(clientAuthClientSSLKeyManagers);

        SSLContextParameters clientAuthClientSSLContextParameters2 = new SSLContextParameters();
        clientAuthClientSSLContextParameters2.setCamelContext(context);
        TrustManagersParameters clientAuthClientSSLTrustManagers2 = new TrustManagersParameters();
        clientAuthClientSSLTrustManagers2.setKeyStore(truststoreParameters2);
        clientAuthClientSSLContextParameters2.setTrustManagers(clientAuthClientSSLTrustManagers2);
        KeyManagersParameters clientAuthClientSSLKeyManagers2 = new KeyManagersParameters();
        clientAuthClientSSLKeyManagers2.setKeyPassword("security");
        clientAuthClientSSLKeyManagers2.setKeyStore(clientKeystoreParameters);
        clientAuthClientSSLContextParameters2.setKeyManagers(clientAuthClientSSLKeyManagers2);

        SSLContextParameters selfSignedClientSSLContextParameters = new SSLContextParameters();
        selfSignedClientSSLContextParameters.setCamelContext(context);
        TrustManagersParameters selfSignedClientSSLTrustManagers = new TrustManagersParameters();
        selfSignedClientSSLTrustManagers.setKeyStore(selfSignedKeyStoreParameters);
        selfSignedClientSSLContextParameters.setTrustManagers(selfSignedClientSSLTrustManagers);

        KeyStore keyStore = serviceKeystoreParameters.createKeyStore();
        PrivateKey privateKey = (PrivateKey) keyStore.getKey("service", "security".toCharArray());
        PublicKey publicKey = keyStore.getCertificate("service").getPublicKey();

        NewAdvancedCertificateVerifier advancedCertificateVerifier = StaticNewAdvancedCertificateVerifier.builder()
                .setTrustAllRPKs().setTrustAllCertificates().build();

        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        AdvancedPskStore advancedPskStore
                = new AdvancedSinglePskStore("some-identity", keyGenerator.generateKey().getEncoded());

        context.getRegistry().bind("serviceSSLContextParameters", serviceSSLContextParameters);
        context.getRegistry().bind("selfSignedServiceSSLContextParameters", selfSignedServiceSSLContextParameters);
        context.getRegistry().bind("clientAuthServiceSSLContextParameters", clientAuthServiceSSLContextParameters);
        context.getRegistry().bind("clientSSLContextParameters", clientSSLContextParameters);
        context.getRegistry().bind("clientSSLContextParameters2", clientSSLContextParameters2);
        context.getRegistry().bind("clientAuthClientSSLContextParameters", clientAuthClientSSLContextParameters);
        context.getRegistry().bind("clientAuthClientSSLContextParameters2", clientAuthClientSSLContextParameters2);
        context.getRegistry().bind("selfSignedClientSSLContextParameters", selfSignedClientSSLContextParameters);

        context.getRegistry().bind("privateKey", privateKey);
        context.getRegistry().bind("publicKey", publicKey);

        context.getRegistry().bind("advancedCertificateVerifier", advancedCertificateVerifier);
        context.getRegistry().bind("advancedPskStore", advancedPskStore);
    }

    protected void sendBodyAndHeader(String endpointUri, final Object body, String headerName, String headerValue) {
        template.send(endpointUri, new Processor() {
            public void process(Exchange exchange) {
                Message in = exchange.getIn();
                in.setBody(body);
                in.setHeader(headerName, headerValue);
            }
        });
    }
}
