package org.apereo.cas.adaptors.x509.authentication.handler.support;

import org.apereo.cas.adaptors.x509.authentication.ExpiredCRLException;
import org.apereo.cas.adaptors.x509.authentication.revocation.policy.AllowRevocationPolicy;
import org.apereo.cas.adaptors.x509.authentication.revocation.checker.CRLDistributionPointRevocationChecker;
import org.apereo.cas.adaptors.x509.authentication.revocation.checker.RevocationChecker;
import org.apereo.cas.adaptors.x509.authentication.revocation.RevokedCertificateException;
import org.apereo.cas.adaptors.x509.authentication.revocation.policy.ThresholdExpiredCRLRevocationPolicy;
import org.apereo.cas.adaptors.x509.util.MockWebServer;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;


/**
 * Unit test for {@link CRLDistributionPointRevocationChecker} class.
 *
 * @author Marvin S. Addison
 * @since 3.4.76
 */
@RunWith(Parameterized.class)
public class CRLDistributionPointRevocationCheckerTests extends AbstractCRLRevocationCheckerTests {
    private static final Logger LOGGER = LoggerFactory.getLogger(CRLDistributionPointRevocationCheckerTests.class);
    
    /**
     * Instance under test.
     */
    private final CRLDistributionPointRevocationChecker checker;

    /**
     * Answers requests for CRLs made to localhost:8085.
     */
    private final MockWebServer webServer;


    /**
     * Creates a new test instance with given parameters.
     *
     * @param checker   Revocation checker instance.
     * @param certFiles File names of certificates to check.
     * @param crlFile   File name of CRL file to serve out.
     * @param expected  Expected result of check; null to indicate expected success.
     */
    public CRLDistributionPointRevocationCheckerTests(
            final CRLDistributionPointRevocationChecker checker,
            final String[] certFiles,
            final String crlFile,
            final GeneralSecurityException expected) throws Exception {

        super(certFiles, expected);

        final File file = new File(System.getProperty("java.io.tmpdir"), "ca.crl");
        if (file.exists()) {
            file.delete();
        }
        final OutputStream out = new FileOutputStream(file);
        IOUtils.copy(new ClassPathResource(crlFile).getInputStream(), out);

        this.checker = checker;

        this.webServer = new MockWebServer(8085, new FileSystemResource(file), "text/plain");
        LOGGER.debug("Web server listening on port 8085 serving file [{}]", crlFile);
    }

    /**
     * Gets the unit test parameters.
     *
     * @return Test parameter data.
     */
    @Parameters
    public static Collection<Object[]> getTestParameters() throws Exception {
        CacheManager.getInstance().removeAllCaches();
        final Collection<Object[]> params = new ArrayList<>();
        Cache cache;
        final ThresholdExpiredCRLRevocationPolicy defaultPolicy = new ThresholdExpiredCRLRevocationPolicy(0);
        final ThresholdExpiredCRLRevocationPolicy zeroThresholdPolicy = new ThresholdExpiredCRLRevocationPolicy(0);

        // Test case #0
        // Valid certificate on valid CRL data with encoded url
        cache = new Cache("crlCache-0", 100, false, false, 20, 10);
        CacheManager.getInstance().addCache(cache);
        params.add(new Object[]{
                new CRLDistributionPointRevocationChecker(cache, defaultPolicy, null),
                new String[]{"uservalid-encoded-crl.crt"},
                "test ca.crl",
                null,
        });

        // Test case #1
        // Valid certificate on valid CRL data
        cache = new Cache("crlCache-1", 100, false, false, 20, 10);
        CacheManager.getInstance().addCache(cache);
        params.add(new Object[]{
                new CRLDistributionPointRevocationChecker(cache, defaultPolicy, null, true),
                new String[]{"user-valid-distcrl.crt"},
                "userCA-valid.crl",
                null,
        });


        // Test case #2
        // Revoked certificate on valid CRL data
        cache = new Cache("crlCache-2", 100, false, false, 20, 10);
        CacheManager.getInstance().addCache(cache);
        params.add(new Object[]{
                new CRLDistributionPointRevocationChecker(cache, defaultPolicy, null),
                new String[]{"user-revoked-distcrl.crt"},
                "userCA-valid.crl",
                new RevokedCertificateException(ZonedDateTime.now(ZoneOffset.UTC), new BigInteger("1")),
        });

        // Test case #3
        // Valid certificate on expired CRL data
        cache = new Cache("crlCache-3", 100, false, false, 20, 10);
        CacheManager.getInstance().addCache(cache);
        params.add(new Object[]{
                new CRLDistributionPointRevocationChecker(cache, zeroThresholdPolicy, null),
                new String[]{"user-valid-distcrl.crt"},
                "userCA-expired.crl",
                new ExpiredCRLException("test", ZonedDateTime.now(ZoneOffset.UTC)),
        });

        // Test case #4
        // Valid certificate on expired CRL data with custom expiration
        // policy to always allow expired CRL data
        cache = new Cache("crlCache-4", 100, false, false, 20, 10);
        CacheManager.getInstance().addCache(cache);
        params.add(new Object[]{
                new CRLDistributionPointRevocationChecker(cache, crl -> {
                }, null),
                new String[]{"user-valid-distcrl.crt"},
                "userCA-expired.crl",
                null,
        });

        // Test case #5
        // Valid certificate with no CRL distribution points defined but with
        // "AllowRevocationPolicy" set to allow unavailable CRL data
        cache = new Cache("crlCache-5", 100, false, false, 20, 10);
        CacheManager.getInstance().addCache(cache);
        final CRLDistributionPointRevocationChecker checker5 =
                new CRLDistributionPointRevocationChecker(cache, defaultPolicy, new AllowRevocationPolicy());
        params.add(new Object[]{
                checker5,
                new String[]{"user-valid.crt"},
                "userCA-expired.crl",
                null,
        });

        // Test case #6
        // EJBCA test case
        // Revoked certificate with CRL distribution point URI that is technically
        // not a valid URI since the issuer DN in the query string is not encoded per
        // the escaping of reserved characters in RFC 2396.
        // Make sure we can convert given URI to valid URI and confirm it's revoked
        cache = new Cache("crlCache-6", 100, false, false, 20, 10);
        CacheManager.getInstance().addCache(cache);
        params.add(new Object[]{
                new CRLDistributionPointRevocationChecker(cache, defaultPolicy, null),
                new String[]{"user-revoked-distcrl2.crt"},
                "userCA-valid.crl",
                new RevokedCertificateException(ZonedDateTime.now(ZoneOffset.UTC), new BigInteger("1")),
        });

        return params;
    }

    /**
     * Called once before every test.
     *
     * @throws Exception On setup errors.
     */
    @Before
    public void setUp() throws Exception {
        this.webServer.start();
        Thread.sleep(500);
    }

    /**
     * Called once before every test.
     *
     * @throws Exception On setup errors.
     */
    @After
    public void tearDown() throws Exception {
        LOGGER.debug("Stopping web server...");
        this.webServer.stop();
        Thread.sleep(500);
        LOGGER.debug("Web server stopped [{}]", !this.webServer.isRunning());
    }

    @AfterClass
    public static void destroy() {
        final File file = new File("ca.crl");
        if (file.exists()) {
            file.delete();
        }
    }

    @Override
    protected RevocationChecker getChecker() {
        return this.checker;
    }
}
