package org.jboss.installer.validators;

import io.undertow.Undertow;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.core.ValidationResult;
import org.jboss.installer.test.utils.CertificateUtils;
import org.jboss.installer.test.utils.MockLanguageUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.wildfly.security.x500.cert.SelfSignedX509CertificateAndSigningKey;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;

import static org.assertj.core.api.Assertions.assertThat;
import static org.jboss.installer.validators.KeystoreCredentialsValidator.KEYSTORE_VALIDATOR_AUTHENTICATION_FAILURE;
import static org.jboss.installer.validators.KeystoreCredentialsValidator.KEYSTORE_VALIDATOR_FILE_DOES_NOT_EXIST;
import static org.jboss.installer.validators.KeystoreCredentialsValidator.KEYSTORE_VALIDATOR_NOT_EMPTY;
import static org.jboss.installer.validators.KeystoreCredentialsValidator.KEYSTORE_VALIDATOR_NOT_SUPPORTED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;

public class KeystoreCredentialsValidatorTest {
    private static final String PREFIX = "test: keystore.";

    @Rule
    public TemporaryFolder tempFolder = new TemporaryFolder();

    private KeystoreCredentialsValidator validator;
    private InstallationData idata;
    private Path testKeystore;

    @Before
    public void setup() throws Exception {
        validator = new KeystoreCredentialsValidator(new MockLanguageUtils(), "test");
        idata = new InstallationData();
        idata.setTargetFolder(Paths.get("target"));

        testKeystore = tempFolder.getRoot().toPath().resolve("test-keystore");
        createTestKeystore(testKeystore.toFile(), "password");
    }

    @After
    public void teardown() throws Exception {
        if (testKeystore.toFile().exists()) {
            testKeystore.toFile().delete();
        }
    }

    @Test
    public void testErrorWhenKeystoreDoesNotExist() throws Exception {
        final ValidationResult result = validator.validate("/foo/bar", "password", idata.getTargetFolder());

        assertEquals(ValidationResult.Result.ERROR, result.getResult());
        assertEquals(PREFIX + KEYSTORE_VALIDATOR_FILE_DOES_NOT_EXIST, result.getMessage());
    }

    @Test
    public void testErrorWhenPasswordIsIncorrect() throws Exception {

        final ValidationResult result = validator.validate(testKeystore.toString(), "wrong_password", idata.getTargetFolder());

        assertEquals(ValidationResult.Result.ERROR, result.getResult());
        assertEquals(PREFIX + KEYSTORE_VALIDATOR_AUTHENTICATION_FAILURE, result.getMessage());
    }

    @Test
    public void testOKWhenPasswordIsCorrect() throws Exception {

        final ValidationResult result = validator.validate(testKeystore.toString(), "password", idata.getTargetFolder());

        assertEquals(ValidationResult.Result.OK, result.getResult());
    }

    @Test
    public void isValidReadableFileTestValidFile() throws Exception {
        File testFile = tempFolder.newFile();
        assertTrue(validator.isValidReadableFile(testFile.getAbsolutePath()));
    }

    @Test
    public void isValidReadableFileTestFileNotExist() throws Exception {
        assertFalse(validator.isValidReadableFile("non-existing-file.txt"));
    }

    @Test
    public void isValidReadableFileTestDirectory() throws Exception {
        assertFalse(validator.isValidReadableFile(tempFolder.getRoot().getAbsolutePath()));
    }

    @Test
    public void isValidReadableFileTestFileNotReadable() throws Exception{
        String OS = System.getProperty("os.name");

        File unreadable = makeUnreadableFileAtBaseDir(tempFolder);
        try {
            //Added so test would be ignored in Docker Containers
            assumeFalse(unreadable.canRead());
            if (!OS.contains("Windows")) {
                assertFalse(validator.isValidReadableFile(unreadable.getAbsolutePath()));
            } else { // windows isn't able to setReadable / setExecutable flags
                assertFalse(unreadable.canWrite());
            }
        } finally {
            revertUnreadableFile(unreadable.getAbsolutePath());
        }
    }

    @Test
    public void isValidAccessibleUrlTestValidUrl() throws Exception {
        Undertow localhost = Undertow.builder().addHttpListener(8123, "localhost", exchange -> exchange.setStatusCode(200)).build();
        try {
            localhost.start();
            assertTrue(validator.isValidAccessibleUrl("http://localhost:8123"));
        } finally {
            localhost.stop();
        }
    }

    @Test
    public void isValidAccessibleUrlTestInvalidUrl() throws Exception {
        Undertow localhost = Undertow.builder().addHttpListener(8123, "localhost", exchange -> exchange.setStatusCode(404)).build();
        try {
            localhost.start();
            // try connecting to non-existing URL
            assertFalse(validator.isValidAccessibleUrl("http://localhost:8124"));
        } finally {
            localhost.stop();
        }
    }

    @Test
    public void testJCEKS() throws Exception {
        File keyStore = tempFolder.newFile("test-key-store.jceks");

        final SelfSignedX509CertificateAndSigningKey selfSignedCertificateAndSigningKey = CertificateUtils.generateSelfSignedCertificate();
        CertificateUtils.generateKeyStore(selfSignedCertificateAndSigningKey, keyStore, "JCEKS");

        ValidationResult result = validator.validate(keyStore.toString(), "wrongpassword", idata.getTargetFolder());
        assertEquals(result.getMessage(), ValidationResult.Result.ERROR, result.getResult());
        assertEquals(PREFIX + KEYSTORE_VALIDATOR_AUTHENTICATION_FAILURE, result.getMessage());

        result = validator.validate(keyStore.toString(), "secret", idata.getTargetFolder());
        assertEquals(result.getMessage(), ValidationResult.Result.OK, result.getResult());
    }

    @Test
    public void testPKCS12() throws Exception {
        File keyStore = tempFolder.newFile("test-key-store.pkc");

        final SelfSignedX509CertificateAndSigningKey selfSignedCertificateAndSigningKey = CertificateUtils.generateSelfSignedCertificate();
        CertificateUtils.generateKeyStore(selfSignedCertificateAndSigningKey, keyStore, "PKCS12");

        ValidationResult result = validator.validate(keyStore.toString(), "secret", idata.getTargetFolder());
        assertEquals(result.getMessage(), ValidationResult.Result.OK, result.getResult());

        result = validator.validate(keyStore.toString(), "wrongpassword", idata.getTargetFolder());
        assertEquals(result.getMessage(), ValidationResult.Result.ERROR, result.getResult());
        assertEquals(PREFIX + KEYSTORE_VALIDATOR_AUTHENTICATION_FAILURE, result.getMessage());
    }

    @Test
    public void testInvalidKeystore() throws Exception {
        File keyStore = tempFolder.newFile("test-key-store.wrong");
        // the file needs to be long enough not to trigger EOFException
        StringBuilder txt = new StringBuilder();
        for (int i=0; i< 1000; i++) {
            txt.append("imwrong\n");
        }
        Files.writeString(keyStore.toPath(), txt);

        ValidationResult result = validator.validate(keyStore.toString(), "secret", idata.getTargetFolder());
        assertEquals(result.getMessage(), ValidationResult.Result.ERROR, result.getResult());
        assertThat(result.getMessage())
                .startsWith(PREFIX + KEYSTORE_VALIDATOR_NOT_SUPPORTED);
    }

    @Test
    public void testNonEmptyJCEKSRequireEmpty() throws Exception {
        File keyStore = tempFolder.newFile("test-key-store.jceks");

        final SelfSignedX509CertificateAndSigningKey selfSignedCertificateAndSigningKey = CertificateUtils.generateSelfSignedCertificate();
        CertificateUtils.generateKeyStore(selfSignedCertificateAndSigningKey, keyStore, "JCEKS");

        ValidationResult result = validator.validate(keyStore.toString(), "wrongpassword", idata.getTargetFolder());
        assertEquals(result.getMessage(), ValidationResult.Result.ERROR, result.getResult());
        assertEquals(PREFIX + KEYSTORE_VALIDATOR_AUTHENTICATION_FAILURE, result.getMessage());

        result = validator.validate(keyStore.toString(), "secret", idata.getTargetFolder(), true);
        assertEquals(result.getMessage(), ValidationResult.Result.ERROR, result.getResult());
        assertEquals(result.getMessage(), PREFIX + KEYSTORE_VALIDATOR_NOT_EMPTY);
    }

    @Test
    public void testEmptyJCEKSRequireEmpty() throws Exception {
        File keyStore = tempFolder.newFile("test-key-store.jceks");

        CertificateUtils.generateKeyStore(null, keyStore, "JCEKS");

        ValidationResult result = validator.validate(keyStore.toString(), "wrongpassword", idata.getTargetFolder());
        assertEquals(result.getMessage(), ValidationResult.Result.ERROR, result.getResult());
        assertEquals(PREFIX + KEYSTORE_VALIDATOR_AUTHENTICATION_FAILURE, result.getMessage());

        result = validator.validate(keyStore.toString(), "secret", idata.getTargetFolder(), true);
        assertEquals(result.getMessage(), ValidationResult.Result.OK, result.getResult());
    }

    static void createTestKeystore(File keystoreFile, String password) throws Exception {
        KeyStore ks = KeyStore.getInstance("jks");

        ks.load(null, password.toCharArray());

        // Store away the keystore.
        try (FileOutputStream fos = new FileOutputStream(keystoreFile)) {
            ks.store(fos, password.toCharArray());
        }
    }

    private File makeUnreadableFileAtBaseDir(TemporaryFolder folder) throws Exception {
        File file = folder.newFile();
        file.createNewFile();
        file.setWritable(false,false);
        file.setReadable(false,false);
        file.setExecutable(false,false);

        return file;
    }

    private void revertUnreadableFile(String path) throws Exception {
        File file = new File(path);
        file.setReadable(true);
        file.setWritable(true);
        file.setExecutable(true);
    }
}