/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.domain.management.security.auditlog;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.COMPOSITE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_DESCRIPTION_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STEPS;

import java.io.File;
import java.io.FileOutputStream;
import java.net.InetAddress;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.regex.Pattern;

import javax.security.auth.x500.X500Principal;

import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.audit.SyslogAuditLogHandler.MessageTransfer;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.domain.management.audit.AuditLogHandlerResourceDefinition;
import org.jboss.as.domain.management.audit.AuditLogLoggerResourceDefinition;
import org.jboss.as.domain.management.audit.FileAuditLogHandlerResourceDefinition;
import org.jboss.as.domain.management.audit.JsonAuditLogFormatterResourceDefinition;
import org.jboss.as.domain.management.audit.SyslogAuditLogHandlerResourceDefinition;
import org.jboss.dmr.ModelNode;
import org.jboss.logmanager.handlers.SyslogHandler.SyslogType;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.wildfly.security.x500.cert.SelfSignedX509CertificateAndSigningKey;

/**
 * Don't use core-model test for this. It does not support runtime, and more importantly for backwards compatibility the audit logger cannot be used
 *
 * @author Kabir Khan
 */
public class AuditLogHandlerBootEnabledTestCase extends AbstractAuditLogHandlerTestCase {
    public AuditLogHandlerBootEnabledTestCase() {
        super(true, true);
    }

    private static final char[] KEYSTORE_CREATION_PASSWORD = "changeit".toCharArray();
    private static final String WORKING_DIRECTORY_LOCATION = "./target/test-classes/org/jboss/as/domain/management/security/auditlog";
    private static final String ALIAS = "selfsigned";
    private static final String CLIENT_DNS_STRING = "CN=localhost, OU=Test, L=Test, ST=Test, C=Test";
    private static final String SERVER_DNS_STRING = "CN=localhost, OU=Unknown, L=Unknown, ST=Unknown, C=Unknown";

    private static void createKeyStoreTrustStore(KeyStore keyStore, KeyStore trustStore, String DN) throws Exception {
        X500Principal principal = new X500Principal(DN);

        SelfSignedX509CertificateAndSigningKey selfSignedX509CertificateAndSigningKey = SelfSignedX509CertificateAndSigningKey.builder()
                .setKeyAlgorithmName("RSA")
                .setSignatureAlgorithmName("SHA1withRSA")
                .setDn(principal)
                .setKeySize(1024)
                .build();
        X509Certificate certificate = selfSignedX509CertificateAndSigningKey.getSelfSignedCertificate();

        keyStore.setKeyEntry(ALIAS, selfSignedX509CertificateAndSigningKey.getSigningKey(), KEYSTORE_CREATION_PASSWORD, new X509Certificate[]{certificate});
        trustStore.setCertificateEntry(ALIAS, certificate);
    }

    private static KeyStore loadKeyStore() throws Exception {
        KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(null, null);
        return ks;
    }

    private static void createTemporaryKeyStoreFile(KeyStore keyStore, File outputFile) throws Exception {
        try (FileOutputStream fos = new FileOutputStream(outputFile)){
            keyStore.store(fos, KEYSTORE_CREATION_PASSWORD);
        }
    }

    @BeforeClass
    public static void initTests() throws Exception {
        KeyStore clientKeyStore = loadKeyStore();
        KeyStore clientTrustStore = loadKeyStore();
        KeyStore serverKeyStore = loadKeyStore();
        KeyStore serverTrustStore = loadKeyStore();

        createKeyStoreTrustStore(clientKeyStore, serverTrustStore, CLIENT_DNS_STRING);
        createKeyStoreTrustStore(serverKeyStore, clientTrustStore, SERVER_DNS_STRING);

        File clientKeyFile = new File(WORKING_DIRECTORY_LOCATION, "client-cert-store.jks");
        File clientTrustFile = new File(WORKING_DIRECTORY_LOCATION, "client-trust-store.jks");
        File serverKeyFile = new File(WORKING_DIRECTORY_LOCATION, "server-cert-store.jks");
        File serverTrustFile = new File(WORKING_DIRECTORY_LOCATION, "server-trust-store.jks");

        createTemporaryKeyStoreFile(clientKeyStore, clientKeyFile);
        createTemporaryKeyStoreFile(clientTrustStore, clientTrustFile);
        createTemporaryKeyStoreFile(serverKeyStore, serverKeyFile);
        createTemporaryKeyStoreFile(serverTrustStore, serverTrustFile);
    }

    @AfterClass
    public static void cleanUpTests() {
        File[] testFiles = {
            new File(WORKING_DIRECTORY_LOCATION, "client-cert-store.jks"),
            new File(WORKING_DIRECTORY_LOCATION, "client-trust-store.jks"),
            new File(WORKING_DIRECTORY_LOCATION, "server-cert-store.jks"),
            new File(WORKING_DIRECTORY_LOCATION, "server-trust-store.jks")
        };
        for (File file : testFiles) {
            if (file.exists()) {
                file.delete();
            }
        }
    }

    @Test
    public void testAuditLoggerBootUp() throws Exception {
        File file = new File(logDir, "test-file.log");
        List<ModelNode> bootRecords = readFile(file, 1);

        ModelNode bootRecord = bootRecords.get(0);
        List<ModelNode> bootOps = checkBootRecordHeader(bootRecord, 5, "core", false, true, true);
        for (int i = 0 ; i < 5 ; i++) {
            checkOpsEqual(bootOperations.get(i), bootOps.get(i));
        }
    }

    @Test
    public void testCannotRemoveReferencedHandler() throws Exception {
        File file = new File(logDir, "test-file.log");

        ModelNode op = createRemoveFileHandlerOperation("file");
        executeForFailure(op);
        List<ModelNode> records = readFile(file, 2);
        //TODO This gets picked up as read-only since it did not actually get around to modifying the model
        List<ModelNode> ops = checkBootRecordHeader(records.get(1), 1, "core", true, false, false);
        checkOpsEqual(op, ops.get(0));
    }

    @Test
    public void testCanRemoveLoggerReference() throws Exception {
        File file = new File(logDir, "test-file.log");

        ModelNode op = createRemoveHandlerReferenceOperation("test-file");
        executeForResult(op);
        List<ModelNode> records = readFile(file, 2);
        List<ModelNode> ops = checkBootRecordHeader(records.get(1), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));
    }

    @Test
    public void testCannotRemoveReferencedFormatter() throws Exception {
        File file = new File(logDir, "test-file.log");

        ModelNode op = createRemoveJsonFormatterOperation("test-formatter");
        executeForFailure(op);
        List<ModelNode> records = readFile(file, 2);
        //TODO This gets picked up as read-only since it did not actually get around to modifying the model
        List<ModelNode> ops = checkBootRecordHeader(records.get(1), 1, "core", true, false, false);
        checkOpsEqual(op, ops.get(0));

        op = createRemoveHandlerReferenceOperation("test-file");
        executeForResult(op);
        records = readFile(file, 3);
        ops = checkBootRecordHeader(records.get(2), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = createRemoveFileHandlerOperation("test-file");
        executeForResult(op);
        readFile(file, 3);

        op = createRemoveJsonFormatterOperation("test-formatter");
        executeForResult(op);
        readFile(file, 3);
    }

    @Test
    public void testDisableAndEnableAuditLogger() throws Exception {
        File file = new File(logDir, "test-file.log");
        readFile(file, 1);

        ModelNode op = createAuditLogWriteAttributeOperation(AuditLogLoggerResourceDefinition.ENABLED.getName(), false);
        executeForResult(op);
        List<ModelNode> records = readFile(file, 2);
        List<ModelNode> ops = checkBootRecordHeader(records.get(1), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = Util.createOperation(READ_RESOURCE_OPERATION, PathAddress.EMPTY_ADDRESS);
        executeForResult(op);
        readFile(file, 2);

        op = createAuditLogWriteAttributeOperation(AuditLogLoggerResourceDefinition.ENABLED.getName(), false);
        executeForResult(op);
        readFile(file, 2);

        op = createAuditLogWriteAttributeOperation(AuditLogLoggerResourceDefinition.ENABLED.getName(), true);
        executeForResult(op);
        records = readFile(file, 3);
        ops = checkBootRecordHeader(records.get(2), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));


        op = Util.createOperation(READ_RESOURCE_OPERATION, PathAddress.EMPTY_ADDRESS);
        executeForResult(op);
        records = readFile(file, 4);
        ops = checkBootRecordHeader(records.get(3), 1, "core", true, false, true);
        checkOpsEqual(op, ops.get(0));
    }

    @Test
    public void testToggleReadOnly() throws Exception {
        File file = new File(logDir, "test-file.log");
        readFile(file, 1);

        ModelNode op = createAuditLogWriteAttributeOperation(AuditLogLoggerResourceDefinition.LOG_READ_ONLY.getName(), false);
        executeForResult(op);
        List<ModelNode> records = readFile(file, 2);
        List<ModelNode> ops = checkBootRecordHeader(records.get(1), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = Util.createOperation(READ_RESOURCE_OPERATION, PathAddress.EMPTY_ADDRESS);
        executeForResult(op);
        readFile(file, 2);

        op = createAuditLogWriteAttributeOperation(AuditLogLoggerResourceDefinition.LOG_READ_ONLY.getName(), true);
        executeForResult(op);
        records = readFile(file, 3);
        ops = checkBootRecordHeader(records.get(2), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = Util.createOperation(READ_RESOURCE_OPERATION, PathAddress.EMPTY_ADDRESS);
        executeForResult(op);
        records = readFile(file, 4);
        ops = checkBootRecordHeader(records.get(3), 1, "core", true, false, true);
        checkOpsEqual(op, ops.get(0));
    }

    @Test
    public void testCannotAddHandlerWithSameName() throws Exception {
        File file = new File(logDir, "test-file.log");

        ModelNode op = createAddFileHandlerOperation("test-file", "test-formatter", "fail.log");
        executeForFailure(op);
        List<ModelNode> records = readFile(file, 2);
        List<ModelNode> ops = checkBootRecordHeader(records.get(1), 1, "core", false, false, false);
        checkOpsEqual(op, ops.get(0));

        op = createAddSyslogHandlerTcpOperation("test-file", "test-formatter", InetAddress.getByName("localhost"), SYSLOG_PORT, null, MessageTransfer.OCTET_COUNTING);
        executeForFailure(op);
        records = readFile(file, 3);
        ops = checkBootRecordHeader(records.get(2), 1, "core", false, false, false);
        checkOpsEqual(op, ops.get(0));

        op = createAddSizeRotatingFileHandlerOperation("test-file", "test-formatter", "fail.log", "10m", 1);
        executeForFailure(op);
        records = readFile(file, 4);
        ops = checkBootRecordHeader(records.get(3), 1, "core", false, false, false);
        checkOpsEqual(op, ops.get(0));

        op = createAddPeriodicRotatingFileHandlerOperation("test-file", "test-formatter", "fail.log", ".yyyy-MM-dd-hh");
        executeForFailure(op);
        records = readFile(file, 5);
        ops = checkBootRecordHeader(records.get(4), 1, "core", false, false, false);
        checkOpsEqual(op, ops.get(0));
    }

    @Test
    public void testAddNonExistingHandlerReference() throws Exception {
        File file = new File(logDir, "test-file.log");

        ModelNode op = createAddHandlerReferenceOperation("notthere");
        executeForFailure(op);
        List<ModelNode> records = readFile(file, 2);
        List<ModelNode> ops = checkBootRecordHeader(records.get(1), 1, "core", false, false, false);
        checkOpsEqual(op, ops.get(0));
    }

    @Test
    public void testSyslogUdp() throws Exception {
        SimpleSyslogServer server = SimpleSyslogServer.createUdp(6666);
        runSyslogTest(server, 6666, createAddSyslogHandlerUdpOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666, null, 0));
    }

    @Test
    public void testSyslogTcpOctetCounting() throws Exception {
        SimpleSyslogServer server = SimpleSyslogServer.createTcp(6666, true);
        runSyslogTest(server, 6666, createAddSyslogHandlerTcpOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666, null, MessageTransfer.OCTET_COUNTING));
    }

    @Test
    public void testSyslogTcpNonTransparentFraming() throws Exception {
        SimpleSyslogServer server = SimpleSyslogServer.createTcp(6666, false);
        runSyslogTest(server, 6666, createAddSyslogHandlerTcpOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666, null, MessageTransfer.NON_TRANSPARENT_FRAMING));
    }

    @Test
    public void testSyslogTlsOctetCounting() throws Exception {
        File serverCertStore = new File(getClass().getResource("server-cert-store.jks").toURI());
        File clientTrustStore = new File(getClass().getResource("client-trust-store.jks").toURI());
        SimpleSyslogServer server = SimpleSyslogServer.createTls(6666, true, serverCertStore, "changeit", null, null);
        runSyslogTest(server, 6666, createAddSyslogHandlerTlsOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666,
                null, MessageTransfer.OCTET_COUNTING, clientTrustStore, "changeit", null, null));
    }

    @Test
    public void testSyslogTlsNonTransparentFraming() throws Exception {
        File serverCertStore = new File(getClass().getResource("server-cert-store.jks").toURI());
        File clientTrustStore = new File(getClass().getResource("client-trust-store.jks").toURI());
        SimpleSyslogServer server = SimpleSyslogServer.createTls(6666, false, serverCertStore, "changeit", null, null);
        runSyslogTest(server, 6666, createAddSyslogHandlerTlsOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666,
                null, MessageTransfer.NON_TRANSPARENT_FRAMING, clientTrustStore, "changeit", null, null));
    }

    @Test
    public void testSyslogTlsOctetCountingClientAuth() throws Exception {
        File serverCertStore = new File(getClass().getResource("server-cert-store.jks").toURI());
        File clientTrustStore = new File(getClass().getResource("client-trust-store.jks").toURI());
        File clientCertStore = new File(getClass().getResource("client-cert-store.jks").toURI());
        File serverTrustStore = new File(getClass().getResource("server-trust-store.jks").toURI());
        SimpleSyslogServer server = SimpleSyslogServer.createTls(6666, true, serverCertStore, "changeit", serverTrustStore, "changeit");
        runSyslogTest(server, 6666, createAddSyslogHandlerTlsOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666,
                null, MessageTransfer.OCTET_COUNTING, clientTrustStore, "changeit", clientCertStore, "changeit"));
    }

    @Test
    public void testSyslogTlsNonTransparentFramingClientAuth() throws Exception {
        File serverCertStore = new File(getClass().getResource("server-cert-store.jks").toURI());
        File clientTrustStore = new File(getClass().getResource("client-trust-store.jks").toURI());
        File clientCertStore = new File(getClass().getResource("client-cert-store.jks").toURI());
        File serverTrustStore = new File(getClass().getResource("server-trust-store.jks").toURI());
        SimpleSyslogServer server = SimpleSyslogServer.createTls(6666, false, serverCertStore, "changeit", serverTrustStore, "changeit");
        runSyslogTest(server, 6666, createAddSyslogHandlerTlsOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666,
                null, MessageTransfer.NON_TRANSPARENT_FRAMING, clientTrustStore, "changeit", clientCertStore, "changeit"));
    }


    @Test
    public void testSyslogTlsNonTransparentFramingClientAuthCredentialReference() throws Exception {
        File serverCertStore = new File(getClass().getResource("server-cert-store.jks").toURI());
        File clientTrustStore = new File(getClass().getResource("client-trust-store.jks").toURI());
        File clientCertStore = new File(getClass().getResource("client-cert-store.jks").toURI());
        File serverTrustStore = new File(getClass().getResource("server-trust-store.jks").toURI());
        SimpleSyslogServer server = SimpleSyslogServer.createTls(6666, false, serverCertStore, "changeit", serverTrustStore, "changeit");
        runSyslogTest(server, 6666, createAddSyslogHandlerCredentialReferenceTlsOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666,
                null, MessageTransfer.NON_TRANSPARENT_FRAMING, clientTrustStore, "changeit", clientCertStore, "changeit"));
    }

    private void runSyslogTest(SimpleSyslogServer server, int port, ModelNode handlerAddOperation) throws Exception {
        try {
            File file = new File(logDir, "test-file.log");

            executeForResult(handlerAddOperation);
            List<ModelNode> records1 = readFile(file, 2);
            List<ModelNode> ops = checkBootRecordHeader(records1.get(1), 1, "core", false, false, true);
            checkOpsEqual(handlerAddOperation, ops.get(0));

            ModelNode op = createAddHandlerReferenceOperation("syslog-test");
            executeForResult(op);
            records1 = readFile(file, 3);
            ops = checkBootRecordHeader(records1.get(2), 1, "core", false, false, true);
            checkOpsEqual(op, ops.get(0));
            byte[] receivedBytes = server.receiveData();
            List<ModelNode> syslogOps = checkBootRecordHeader(getSyslogRecord(receivedBytes), 1, "core", false, false, true);
            Assert.assertEquals(ops, syslogOps);
            //TODO check syslog format and contents

            op = Util.createOperation(READ_RESOURCE_DESCRIPTION_OPERATION, PathAddress.EMPTY_ADDRESS);
            executeForResult(op);
            records1 = readFile(file, 4);
            ops = checkBootRecordHeader(records1.get(3), 1, "core", true, false, true);
            checkOpsEqual(op, ops.get(0));
            receivedBytes = server.receiveData();
            syslogOps = checkBootRecordHeader(getSyslogRecord(receivedBytes), 1, "core", true, false, true);
            Assert.assertEquals(ops, syslogOps);

            //Make sure that removing the syslog handler it still gets logged
            op = createRemoveHandlerReferenceOperation("syslog-test");
            executeForResult(op);
            records1 = readFile(file, 5);
            ops = checkBootRecordHeader(records1.get(4), 1, "core", false, false, true);
            checkOpsEqual(op, ops.get(0));
            receivedBytes = server.receiveData();
            syslogOps = checkBootRecordHeader(getSyslogRecord(receivedBytes), 1, "core", false, false, true);
            Assert.assertEquals(ops, syslogOps);

            op = Util.createOperation(READ_RESOURCE_DESCRIPTION_OPERATION, PathAddress.EMPTY_ADDRESS);
            executeForResult(op);
            records1 = readFile(file, 6);
            ops = checkBootRecordHeader(records1.get(5), 1, "core", true, false, true);
            checkOpsEqual(op, ops.get(0));
            //Should be nothing in syslog
            Assert.assertNull(server.pollData());

            //TODO remove handler and check nothing in syslog and expected in file
        } finally {
            server.close();
        }
    }


    @Test
    public void testAddRemoveFileAuditLogHandler() throws Exception {
        File file1 = new File(logDir, "test-file.log");
        File file2 = new File(logDir, "test-file2.log");
        Assert.assertFalse(file2.exists());

        ModelNode op = createAddFileHandlerOperation("file2", "test-formatter", "test-file2.log");
        executeForResult(op);
        Assert.assertFalse(file2.exists());
        List<ModelNode> records1 = readFile(file1, 2);
        List<ModelNode> ops = checkBootRecordHeader(records1.get(1), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = createAddHandlerReferenceOperation("file2");
        executeForResult(op);
        records1 = readFile(file1, 3);
        List<ModelNode> records2 = readFile(file2, 1);
        Assert.assertEquals(records1.get(2), records2.get(0));
        ops = checkBootRecordHeader(records1.get(2), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = createRemoveHandlerReferenceOperation("test-file");
        executeForResult(op);
        records1 = readFile(file1, 4);
        records2 = readFile(file2, 2);
        Assert.assertEquals(records1.get(3), records2.get(1));
        ops = checkBootRecordHeader(records1.get(3), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        //The file handler was closed so it creates a new file
        op = createAddHandlerReferenceOperation("test-file");
        executeForResult(op);
        records1 = readFile(file1, 1);
        records2 = readFile(file2, 3);
        Assert.assertEquals(records1.get(0), records2.get(2));
        ops = checkBootRecordHeader(records1.get(0), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = createRemoveHandlerReferenceOperation("test-file");
        executeForResult(op);
        records1 = readFile(file1, 2);
        records2 = readFile(file2, 4);
        Assert.assertEquals(records1.get(1), records2.get(3));
        ops = checkBootRecordHeader(records1.get(1), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));

        op = createRemoveFileHandlerOperation("test-file");
        executeForResult(op);
        records1 = readFile(file1, 2);
        records2 = readFile(file2, 5);
        ops = checkBootRecordHeader(records2.get(4), 1, "core", false, false, true);
        checkOpsEqual(op, ops.get(0));
    }

    @Test
    public void testAddRemoveSyslogAuditLogHandler() throws Exception {
        File file1 = new File(logDir, "test-file.log");

        SimpleSyslogServer server = SimpleSyslogServer.createTcp(6666, false);
        try {
            ModelNode op = createAddSyslogHandlerTcpOperation("tcp", "test-formatter", InetAddress.getByName("localhost"), SYSLOG_PORT, null, null);
            executeForResult(op);
            List<ModelNode> records1 = readFile(file1, 2);
            List<ModelNode> ops = checkBootRecordHeader(records1.get(1), 1, "core", false, false, true);
            checkOpsEqual(op, ops.get(0));

            op = createAddHandlerReferenceOperation("tcp");
            executeForResult(op);
            records1 = readFile(file1, 3);
            ops = checkBootRecordHeader(records1.get(2), 1, "core", false, false, true);
            ModelNode syslogOp = checkBootRecordHeader(getSyslogRecord(server.receiveData()), 1, "core", false, false, true).get(0);
            checkOpsEqual(op, syslogOp);
            Assert.assertEquals(syslogOp, ops.get(0));

            op = createRemoveHandlerReferenceOperation("tcp");
            executeForResult(op);
            records1 = readFile(file1, 4);
            ops = checkBootRecordHeader(records1.get(3), 1, "core", false, false, true);
            syslogOp = checkBootRecordHeader(getSyslogRecord(server.receiveData()), 1, "core", false, false, true).get(0);
            checkOpsEqual(op, syslogOp);
            Assert.assertEquals(syslogOp, ops.get(0));
        } finally {
            //Close the dummy server since it only accepts one connection
            server.close();
        }

        server = SimpleSyslogServer.createTcp(6666, false);
        try {
            ModelNode op = createAddHandlerReferenceOperation("tcp");
            executeForResult(op);
            List<ModelNode> records1 = readFile(file1, 5);
            List<ModelNode> ops = checkBootRecordHeader(records1.get(4), 1, "core", false, false, true);
            ModelNode syslogOp = checkBootRecordHeader(getSyslogRecord(server.receiveData()), 1, "core", false, false, true).get(0);
            checkOpsEqual(op, syslogOp);
            Assert.assertEquals(syslogOp, ops.get(0));

            op = createRemoveHandlerReferenceOperation("test-file");
            executeForResult(op);
            records1 = readFile(file1, 6);
            //records2 = readFile(file2, 4);
            //Assert.assertEquals(records1.get(5), records2.get(3));
            ops = checkBootRecordHeader(records1.get(5), 1, "core", false, false, true);
            checkOpsEqual(op, ops.get(0));
        } finally {
            server.close();
        }
    }

    @Test
    public void testMessageTransfer() throws Exception {
        SimpleSyslogServer server1 = SimpleSyslogServer.createUdp(SYSLOG_PORT);
        try {
            SimpleSyslogServer server2 = SimpleSyslogServer.createUdp(SYSLOG_PORT2);
            try {
                executeForResult(createAddSyslogHandlerUdpOperation("rfc-3164", "test-formatter", InetAddress.getByName("localhost"), SYSLOG_PORT, SyslogType.RFC3164, 5000));
                executeForResult(createAddSyslogHandlerUdpOperation("rfc-5424", "test-formatter", InetAddress.getByName("localhost"), SYSLOG_PORT2, SyslogType.RFC5424, 5000));

                ModelNode composite = new ModelNode();
                composite.get(OP_ADDR).setEmptyList();
                composite.get(OP).set(COMPOSITE);
                composite.get(STEPS).add(createAddHandlerReferenceOperation("rfc-3164"));
                composite.get(STEPS).add(createAddHandlerReferenceOperation("rfc-5424"));
                executeForResult(composite);

                byte[] bytes1 = server1.receiveData();
                byte[] bytes2 = server2.receiveData();

                //The RFC-5424 format is longer than the RFC-3164 format
                Assert.assertTrue(bytes2.length > bytes1.length);

                ModelNode op1 = checkBootRecordHeader(getSyslogRecord(bytes1), 1, "core", false, false, true).get(0);
                checkOpsEqual(composite, op1);
                ModelNode op2 = checkBootRecordHeader(getSyslogRecord(bytes2), 1, "core", false, false, true).get(0);
                Assert.assertEquals(op1, op2);
            } finally {
                server2.close();
            }
        } finally {
            server1.close();
        }
    }

    @Test
    public void testUpdateFileHandlerFormatter() throws Exception {
        //testUpdateSyslogHandlerFormatter Does the same for the syslog hander
        File file = new File(logDir, "test-file.log");
        String fullRecord = readFullFileRecord(file);
        Assert.assertTrue(Pattern.matches("\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d - \\{[\\s\\S]*", fullRecord)); //This regexp allows for new lines

        file.delete();
        ModelNode op = Util.getWriteAttributeOperation(createFileHandlerAddress("test-file"),
                FileAuditLogHandlerResourceDefinition.FORMATTER.getName(),
                new ModelNode("non-existent"));
        executeForFailure(op);
        fullRecord = readFullFileRecord(file);
        Assert.assertTrue(Pattern.matches("\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d - \\{[\\s\\S]*", fullRecord)); //This regexp allows for new lines
        ModelNode record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
        ModelNode loggedOp = checkBootRecordHeader(record, 1, "core", false, false, false).get(0);
        checkOpsEqual(op, loggedOp);


        //Add some new formatters
        op = Util.createAddOperation(createJsonFormatterAddress("compact-formatter"));
        op.get(JsonAuditLogFormatterResourceDefinition.COMPACT.getName()).set(true);
        op.get(JsonAuditLogFormatterResourceDefinition.DATE_FORMAT.getName()).set("yyyy/MM/dd HH-mm-ss");
        op.get(JsonAuditLogFormatterResourceDefinition.DATE_SEPARATOR.getName()).set(" xxx ");
        executeForResult(op);

        op = Util.createAddOperation(createJsonFormatterAddress("escaped-formatter"));
        op.get(JsonAuditLogFormatterResourceDefinition.INCLUDE_DATE.getName()).set(false);
        op.get(JsonAuditLogFormatterResourceDefinition.ESCAPE_NEW_LINE.getName()).set(true);
        executeForResult(op);

        //Update the handler formatter to the compact version and check the logged format
        file.delete();
        op = Util.getWriteAttributeOperation(createFileHandlerAddress("test-file"), FileAuditLogHandlerResourceDefinition.FORMATTER.getName(), new ModelNode("compact-formatter"));
        executeForResult(op);
        fullRecord = readFullFileRecord(file);
        Assert.assertTrue(Pattern.matches("\\d\\d\\d\\d/\\d\\d/\\d\\d \\d\\d-\\d\\d-\\d\\d xxx \\{.*", fullRecord)); //This regexp checks for no new lines
        record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
        loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
        checkOpsEqual(op, loggedOp);

        //Update the handler formatter to the escaped version and check the logged format
        file.delete();
        op = Util.getWriteAttributeOperation(createFileHandlerAddress("test-file"), FileAuditLogHandlerResourceDefinition.FORMATTER.getName(), new ModelNode("escaped-formatter"));
        executeForResult(op);
        fullRecord = readFullFileRecord(file);
        Assert.assertTrue(Pattern.matches("\\{.*", fullRecord)); //This regexp checks for no new lines
        Assert.assertTrue(fullRecord.indexOf("#012") > 0);
        record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')).replace("#012", ""));
        loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
        checkOpsEqual(op, loggedOp);

        //Check removing formatter in use fails
        file.delete();
        op = Util.createRemoveOperation(createJsonFormatterAddress("escaped-formatter"));
        executeForFailure(op);

        //Check can remove unused formatter
        op = Util.createRemoveOperation(createJsonFormatterAddress("compact-formatter"));
        executeForResult(op);

        //Now try changing the used formatter at runtime
        file.delete();
        op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.ESCAPE_NEW_LINE.getName(), ModelNode.FALSE);
        executeForResult(op);
        fullRecord = readFullFileRecord(file);
        Assert.assertTrue(Pattern.matches("\\{[\\s\\S]*", fullRecord)); //This regexp allows for new lines
        Assert.assertTrue(fullRecord.indexOf("#012") == -1);
        record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
        loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
        checkOpsEqual(op, loggedOp);

        file.delete();
        op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.COMPACT.getName(), ModelNode.TRUE);
        executeForResult(op);
        fullRecord = readFullFileRecord(file);
        Assert.assertTrue(Pattern.matches("\\{.*", fullRecord)); //This regexp allows for new lines
        Assert.assertTrue(fullRecord.indexOf("#012") == -1);
        record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
        loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
        checkOpsEqual(op, loggedOp);

        op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.INCLUDE_DATE.getName(), ModelNode.TRUE);
        executeForResult(op);
        op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.DATE_FORMAT.getName(), new ModelNode("yyyy/MM/dd HH-mm-ss"));
        executeForResult(op);
        file.delete();
        op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.DATE_SEPARATOR.getName(), new ModelNode(" xxx "));
        executeForResult(op);
        fullRecord = readFullFileRecord(file);
        Assert.assertTrue(Pattern.matches("\\d\\d\\d\\d/\\d\\d/\\d\\d \\d\\d-\\d\\d-\\d\\d xxx \\{.*", fullRecord)); //This regexp checks for no new lines
        record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
        loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
        checkOpsEqual(op, loggedOp);
    }


    @Test
    public void testUpdateSyslogHandlerFormatter() throws Exception {
        //testUpdateFileHandlerFormatter does the same for the file handler
        SimpleSyslogServer server = SimpleSyslogServer.createUdp(6666);

        try {
            //Set up the syslog handler
            ModelNode op = createAddSyslogHandlerUdpOperation("syslog-test", "test-formatter", InetAddress.getByName("localhost"), 6666, null, 0);
            executeForResult(op);
            op = createAddHandlerReferenceOperation("syslog-test");
            executeForResult(op);
            byte[] bytes = server.receiveData();

            op = Util.getWriteAttributeOperation(createFileHandlerAddress("test-file"),
                    FileAuditLogHandlerResourceDefinition.FORMATTER.getName(),
                    new ModelNode("non-existent"));
            executeForFailure(op);
            bytes = server.receiveData();
            String fullRecord = stripSyslogHeader(bytes);
            Assert.assertTrue(Pattern.matches("\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d - \\{[\\s\\S]*", fullRecord)); //This regexp allows for new lines
            ModelNode record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
            ModelNode loggedOp = checkBootRecordHeader(record, 1, "core", false, false, false).get(0);
            checkOpsEqual(op, loggedOp);

            //Add some new formatters
            op = Util.createAddOperation(createJsonFormatterAddress("compact-formatter"));
            op.get(JsonAuditLogFormatterResourceDefinition.COMPACT.getName()).set(true);
            op.get(JsonAuditLogFormatterResourceDefinition.DATE_FORMAT.getName()).set("yyyy/MM/dd HH-mm-ss");
            op.get(JsonAuditLogFormatterResourceDefinition.DATE_SEPARATOR.getName()).set(" xxx ");
            executeForResult(op);
            bytes = server.receiveData();

            op = Util.createAddOperation(createJsonFormatterAddress("escaped-formatter"));
            op.get(JsonAuditLogFormatterResourceDefinition.INCLUDE_DATE.getName()).set(false);
            op.get(JsonAuditLogFormatterResourceDefinition.ESCAPE_NEW_LINE.getName()).set(true);
            executeForResult(op);
            bytes = server.receiveData();

            //Update the handler formatter to the compact version and check the logged format
            op = Util.getWriteAttributeOperation(createSyslogHandlerAddress("syslog-test"), FileAuditLogHandlerResourceDefinition.FORMATTER.getName(), new ModelNode("compact-formatter"));
            executeForResult(op);
            bytes = server.receiveData();
            fullRecord = stripSyslogHeader(bytes);
            Assert.assertTrue(Pattern.matches("\\d\\d\\d\\d/\\d\\d/\\d\\d \\d\\d-\\d\\d-\\d\\d xxx \\{.*", fullRecord)); //This regexp checks for no new lines
            record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
            loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
            checkOpsEqual(op, loggedOp);

            //Update the handler formatter to the escaped version and check the logged format
            op = Util.getWriteAttributeOperation(createSyslogHandlerAddress("syslog-test"), FileAuditLogHandlerResourceDefinition.FORMATTER.getName(), new ModelNode("escaped-formatter"));
            executeForResult(op);
            bytes = server.receiveData();
            fullRecord = stripSyslogHeader(bytes);
            Assert.assertTrue(Pattern.matches("\\{.*", fullRecord)); //This regexp checks for no new lines
            Assert.assertTrue(fullRecord.indexOf("#012") > 0);
            record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')).replace("#012", ""));
            loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
            checkOpsEqual(op, loggedOp);

            //Check removing formatter in use fails
            op = Util.createRemoveOperation(createJsonFormatterAddress("escaped-formatter"));
            executeForFailure(op);
            bytes = server.receiveData();

            //Check can remove unused formatter
            op = Util.createRemoveOperation(createJsonFormatterAddress("compact-formatter"));
            executeForResult(op);
            bytes = server.receiveData();

            op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.ESCAPE_NEW_LINE.getName(), ModelNode.FALSE);
            executeForResult(op);
            bytes = server.receiveData();
            fullRecord = stripSyslogHeader(bytes);
            Assert.assertTrue(Pattern.matches("\\{[\\s\\S]*", fullRecord)); //This regexp allows for new lines
            Assert.assertTrue(fullRecord.indexOf("#012") == -1);
            record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
            loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
            checkOpsEqual(op, loggedOp);

            op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.COMPACT.getName(), ModelNode.TRUE);
            executeForResult(op);
            bytes = server.receiveData();
            fullRecord = stripSyslogHeader(bytes);
            Assert.assertTrue(Pattern.matches("\\{.*", fullRecord)); //This regexp allows for new lines
            Assert.assertTrue(fullRecord.indexOf("#012") == -1);
            record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
            loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
            checkOpsEqual(op, loggedOp);

            op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.INCLUDE_DATE.getName(), ModelNode.TRUE);
            executeForResult(op);
            bytes = server.receiveData();
            op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.DATE_FORMAT.getName(), new ModelNode("yyyy/MM/dd HH-mm-ss"));
            executeForResult(op);
            bytes = server.receiveData();
            op = Util.getWriteAttributeOperation(createJsonFormatterAddress("escaped-formatter"), JsonAuditLogFormatterResourceDefinition.DATE_SEPARATOR.getName(), new ModelNode(" xxx "));
            executeForResult(op);
            bytes = server.receiveData();
            fullRecord = stripSyslogHeader(bytes);
            Assert.assertTrue(Pattern.matches("\\d\\d\\d\\d/\\d\\d/\\d\\d \\d\\d-\\d\\d-\\d\\d xxx \\{.*", fullRecord)); //This regexp checks for no new lines
            record = ModelNode.fromJSONString(fullRecord.substring(fullRecord.indexOf('{')));
            loggedOp = checkBootRecordHeader(record, 1, "core", false, false, true).get(0);
            checkOpsEqual(op, loggedOp);
        } finally {
            server.close();
        }
    }

    @Test
    public void testRuntimeFailureMetricsAndRecycle() throws Exception {
        ModelNode op = createAddSyslogHandlerTcpOperation("syslog", "test-formatter", InetAddress.getByName("localhost"), SYSLOG_PORT, null, MessageTransfer.OCTET_COUNTING);
        op.get(STEPS).asList().get(0).get(SyslogAuditLogHandlerResourceDefinition.MAX_FAILURE_COUNT.getName()).set(2);
        executeForResult(op);

        final ModelNode readResource = Util.createOperation(READ_RESOURCE_OPERATION, AUDIT_ADDR);
        readResource.get(ModelDescriptionConstants.RECURSIVE).set(true);
        readResource.get(ModelDescriptionConstants.INCLUDE_RUNTIME).set(true);

        ModelNode result = executeForResult(readResource);
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 3, 0, false);
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 2, 0, false);

        //Delete the log directory so we start seeing failures in the file handler
        for (File file : logDir.listFiles()) {
            file.delete();
        }
        logDir.delete();

        executeForResult(createAddHandlerReferenceOperation("syslog"));

        result = executeForResult(readResource);
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 3, 1, false);
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 2, 1, false);

        result = executeForResult(readResource);
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 3, 2, false);
        //syslog handler has been disabled after 2 failures
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 2, 2, true);

        result = executeForResult(readResource);
        //File handler is disabled after 3 failures
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 3, 3, true);
        //syslog handler should still be disabled
        checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 2, 2, true);

        //Install syslog server so it is up and running
        SimpleSyslogServer server = SimpleSyslogServer.createTcp(6666, true);
        try {
            //File handler should still be disabled
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 3, 3, true);
            //syslog handler should still be disabled
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 2, 2, true);

            //Recycle the syslog handler so it reconnects and resets the failure count
            executeForResult(Util.createOperation(ModelDescriptionConstants.RECYCLE, createSyslogHandlerAddress("syslog")));
            server.receiveData();

            //Create the logging directory
            logDir.mkdir();

            result = executeForResult(readResource);
            server.receiveData();
            //File handler should still be disabled
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 3, 3, true);
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 2, 0, false);

            //Recycle the file handler so it resets the failure count and starts logging again
            executeForResult(Util.createOperation(ModelDescriptionConstants.RECYCLE, createFileHandlerAddress("test-file")));
            server.receiveData();

            File file = new File(logDir, "test-file.log");
            Assert.assertTrue(file.exists());

            result = executeForResult(readResource);
            server.receiveData();
            //Both handlers should work again
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 3, 0, false);
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 2, 0, false);

            //Recycle the file handler and make sure it gets backed up
            long fileSize = file.length();
            Assert.assertEquals(1, logDir.listFiles().length);
            executeForResult(Util.createOperation(ModelDescriptionConstants.RECYCLE, createFileHandlerAddress("test-file")));
            server.receiveData();
            Assert.assertEquals(2, logDir.listFiles().length);
            for (File current : logDir.listFiles()) {
                if (current.getName().equals(file.getName())){
                    Assert.assertFalse(fileSize == current.length());
                } else {
                    Assert.assertEquals(fileSize, current.length());
                }
            }

            //Finally just update the max failure counts and see that works
            op = Util.getWriteAttributeOperation(createFileHandlerAddress("test-file"), AuditLogHandlerResourceDefinition.MAX_FAILURE_COUNT.getName(), new ModelNode(7));
            executeForResult(op);
            server.receiveData();
            op = Util.getWriteAttributeOperation(createSyslogHandlerAddress("syslog"), AuditLogHandlerResourceDefinition.MAX_FAILURE_COUNT.getName(), new ModelNode(4));
            executeForResult(op);
            server.receiveData();

            result = executeForResult(readResource);
            server.receiveData();
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.FILE_HANDLER, "test-file"), 7, 0, false);
            checkHandlerRuntimeFailureMetrics(result.get(ModelDescriptionConstants.SYSLOG_HANDLER, "syslog"), 4, 0, false);
        } finally {
            server.close();
        }
    }
}
