/**
 * 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.transport.https;

import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.X509Certificate;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.apache.cxf.configuration.security.CertificateConstraintsType;
import org.apache.cxf.staxutils.StaxUtils;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class CertConstraintsTest {

    @org.junit.Test
    public void testCertConstraints() throws Exception {
        final X509Certificate bethalCert =
            loadCertificate("Bethal.jks", "JKS", "password", "bethal");
        final X509Certificate gordyCert =
            loadCertificate("Gordy.jks", "JKS", "password", "gordy");

        //
        // bethal matches but gordy doesn't
        //
        CertConstraints tmp = loadCertConstraints("subject-CN-bethal");
        assertTrue(tmp.matches(bethalCert) && !tmp.matches(gordyCert));
        //
        // gordy matches but bethal doesn't
        //
        tmp = loadCertConstraints("subject-CN-gordy");
        assertFalse(tmp.matches(bethalCert) && tmp.matches(gordyCert));

        //
        // both are under the ApacheTest organization
        //
        tmp = loadCertConstraints("subject-O-apache");
        assertTrue(tmp.matches(bethalCert) && tmp.matches(gordyCert));

        //
        // only bethal is both CN=Bethal and O=ApacheTest
        //
        tmp = loadCertConstraints("subject-CN-bethal-O-apache");
        assertTrue(tmp.matches(bethalCert) && !tmp.matches(gordyCert));

        //
        // neither are O=BadApacheTest
        //
        tmp = loadCertConstraints("subject-CN-bethal-O-badapache");
        assertFalse(tmp.matches(bethalCert) || tmp.matches(gordyCert));

        //
        // both satisfy either CN=Bethal or O=ApacheTest
        //
        tmp = loadCertConstraints("subject-CN-bethal-O-apache-ANY");
        assertTrue(tmp.matches(bethalCert) && tmp.matches(gordyCert));

        //
        // only Bethal has "Bethal" as an issuer
        //
        tmp = loadCertConstraints("issuer-CN-bethal-O-apache");
        assertTrue(tmp.matches(bethalCert) && !tmp.matches(gordyCert));
    }

    //
    // Private utilities
    //

    private static CertConstraints loadCertConstraints(
        final String id
    ) throws Exception {
        CertificateConstraintsType certsConstraintsType =
            loadCertificateConstraintsType(id);
        return CertConstraintsJaxBUtils.createCertConstraints(certsConstraintsType);
    }

    private static CertificateConstraintsType loadCertificateConstraintsType(
        final String id
    ) throws Exception {
        return loadGeneratedType(
            CertificateConstraintsType.class,
            "certConstraints",
            "resources/cert-constraints.xml",
            id
        );
    }

    private static X509Certificate loadCertificate(
        final String keystoreFilename,
        final String keystoreType,
        final String keystorePassword,
        final String id
    ) throws Exception {
        final KeyStore store = KeyStore.getInstance(keystoreType);
        try (InputStream is = CertConstraintsTest.class
                .getResourceAsStream("/org/apache/cxf/transport/https/resources/" + keystoreFilename)) {
            store.load(is, keystorePassword.toCharArray());
            for (java.util.Enumeration<String> aliases = store.aliases(); aliases.hasMoreElements();) {
                final String alias = aliases.nextElement();
                if (id.equals(alias)) {
                    return (X509Certificate) store.getCertificate(alias);
                }
            }
        }
        throw new RuntimeException("error in test -- keystore " + id + " has no trusted certs");
    }

    private static <T> T loadGeneratedType(
        final Class<T> cls,
        final String elementName,
        final String name,
        final String id
    ) throws Exception {
        final org.w3c.dom.Document doc = loadDocument(name);
        final org.w3c.dom.Element testData = doc.getDocumentElement();
        final org.w3c.dom.NodeList data = testData.getElementsByTagName("datum");
        for (int i = 0;  i < data.getLength();  ++i) {
            final org.w3c.dom.Element datum = (org.w3c.dom.Element) data.item(i);
            if (datum.getAttribute("id").equals(id)) {
                final org.w3c.dom.NodeList elts = datum.getElementsByTagNameNS(
                    "http://cxf.apache.org/configuration/security", elementName
                );
                assertEquals(1, elts.getLength());
                return unmarshal(cls, (org.w3c.dom.Element) elts.item(0));
            }
        }
        throw new Exception("Bad test!  No test data with id " + id);
    }


    private static org.w3c.dom.Document loadDocument(
        final String name
    ) throws Exception {
        try (java.io.InputStream inStream =
            CertConstraintsTest.class.getResourceAsStream(name)) {
            return StaxUtils.read(inStream);
        }
    }

    private static <T> T unmarshal(
        final Class<T> cls,
        final org.w3c.dom.Element elt
    ) throws JAXBException {
        final JAXBContext ctx = JAXBContext.newInstance(cls.getPackage().getName());
        final Unmarshaller unmarshaller = ctx.createUnmarshaller();
        final JAXBElement<T> jaxbElement =
            unmarshaller.unmarshal(elt, cls);
        return jaxbElement.getValue();
    }

}
