/*
 * 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.aries.transaction.internal;

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

import java.io.File;
import java.io.RandomAccessFile;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import javax.transaction.xa.Xid;

import org.apache.commons.io.FileUtils;
import org.apache.geronimo.transaction.log.HOWLLog;
import org.apache.geronimo.transaction.manager.TransactionBranchInfo;
import org.apache.geronimo.transaction.manager.XidFactory;
import org.apache.geronimo.transaction.manager.XidImpl;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogConversionTest {

    public static Logger LOG = LoggerFactory.getLogger(LogConversionTest.class);
    private static XidFactory xidFactory = new TestXidFactoryImpl("org.apache.aries.transaction.test".getBytes());
    private static File BASE = new File(System.getProperty("user.dir"), "txlogs");

    private static long start = 42L;
    private static long count = start;

    @Test
    public void initialConfiguration() throws Exception {
        File logDir = new File(BASE, "initialConfiguration");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Dictionary<String, Object> properties = new Hashtable<String, Object>();
        createLog("initialConfiguration", "transaction", 2, -1, 1, properties);

        assertFalse(TransactionLogUtils.copyActiveTransactions(null, properties));
    }

    @Test
    public void initialConfigurationEmptyTransactionLog() throws Exception {
        File logDir = new File(BASE, "initialConfigurationEmptyTransactionLog");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Dictionary<String, Object> properties = new Hashtable<String, Object>();
        createLog("initialConfigurationEmptyTransactionLog", "transaction", 2, -1, 1, properties);
        new RandomAccessFile(new File(logDir, "transaction_1.log"), "rw").close();
        new RandomAccessFile(new File(logDir, "transaction_2.log"), "rw").close();

        assertFalse(TransactionLogUtils.copyActiveTransactions(null, properties));
    }

    @Test
    public void initialConfigurationExistingTransactionLogNoChanges() throws Exception {
        File logDir = new File(BASE, "initialConfigurationExistingTransactionLogNoChanges");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Dictionary<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("initialConfigurationExistingTransactionLogNoChanges", "transaction", 2, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        long lm = earlierLastModified(new File(logDir, "transaction_1.log"));

        assertFalse(TransactionLogUtils.copyActiveTransactions(null, properties));
        assertEquals("Transaction log should not be touched", new File(logDir, "transaction_1.log").lastModified(), lm);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void existingTransactionLogChangedLogDir() throws Exception {
        File logDir = new File(BASE, "existingTransactionLogChangedLogDir");
        File newLogDir = new File(BASE, "existingTransactionLogChangedLogDir-new");
        FileUtils.deleteDirectory(logDir);
        FileUtils.deleteDirectory(newLogDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("existingTransactionLogChangedLogDir", "transaction", 2, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        long lm = earlierLastModified(new File(logDir, "transaction_1.log"));

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.logFileDir", newLogDir.getAbsolutePath());

        assertFalse(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertEquals("Transaction log should not be touched", new File(newLogDir, "transaction_1.log").lastModified(), lm);
        assertFalse("Old transaction log should be moved", logDir.exists());
        assertTrue("New transaction log should be created", newLogDir.exists());
    }

    @Test
    @SuppressWarnings("unchecked")
    public void unknownExistingTransactionLogChangedLogDir() throws Exception {
        File logDir = new File(BASE, "unknownExistingTransactionLogChangedLogDir");
        File newLogDir = new File(BASE, "unknownExistingTransactionLogChangedLogDir-new");
        FileUtils.deleteDirectory(logDir);
        FileUtils.deleteDirectory(newLogDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("unknownExistingTransactionLogChangedLogDir", "transaction", 2, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.logFileDir", newLogDir.getAbsolutePath());

        assertFalse(TransactionLogUtils.copyActiveTransactions(null, newConfig));
        assertFalse("Transaction log should not exist", new File(newLogDir, "transaction_1.log").exists());
        assertTrue("Old transaction log should not be removed", logDir.exists());
        assertFalse("New transaction log should not be created (will be created on TX Manager startup)", newLogDir.exists());
    }

    @Test
    @SuppressWarnings("unchecked")
    public void changedNumberOfFiles() throws Exception {
        File logDir = new File(BASE, "changedNumberOfFiles");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("changedNumberOfFiles", "transaction", 2, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.maxLogFiles", "20");

        long lm = earlierLastModified(new File(logDir, "transaction_1.log"));

        assertTrue(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertTrue("Transaction log should exist", new File(logDir, "transaction_1.log").exists());
        assertTrue("There should be 20 transaction log files", new File(logDir, "transaction_20.log").exists());
        assertNotEquals("Transaction log should be processed", new File(logDir, "transaction_1.log").lastModified(), lm);
    }

    private long earlierLastModified(File file) {
        file.setLastModified(file.lastModified() - 10000L);
        return file.lastModified();
    }

    @Test
    @SuppressWarnings("unchecked")
    public void changedMaxBlocksPerFile() throws Exception {
        File logDir = new File(BASE, "changedMaxBlocksPerFile");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("changedMaxBlocksPerFile", "transaction", 3, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.maxBlocksPerFile", "20");

        long lm = earlierLastModified(new File(logDir, "transaction_1.log"));

        assertTrue(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertTrue("Transaction log should exist", new File(logDir, "transaction_1.log").exists());
        assertFalse("There should be 3 transaction log files", new File(logDir, "transaction_4.log").exists());
        assertNotEquals("Transaction log should be processed", new File(logDir, "transaction_1.log").lastModified(), lm);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void changedBlockSize() throws Exception {
        File logDir = new File(BASE, "changedBlockSize");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("changedBlockSize", "transaction", 3, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.bufferSize", "32");

        long lm = earlierLastModified(new File(logDir, "transaction_1.log"));

        assertTrue(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertTrue("Transaction log should exist", new File(logDir, "transaction_1.log").exists());
        assertNotEquals("Transaction log should be processed", new File(logDir, "transaction_1.log").lastModified(), lm);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void changed3ParametersAndLogDir() throws Exception {
        File logDir = new File(BASE, "changed3ParametersAndLogDir");
        File newLogDir = new File(BASE, "changed3ParametersAndLogDir-new");
        FileUtils.deleteDirectory(logDir);
        FileUtils.deleteDirectory(newLogDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("changed3ParametersAndLogDir", "transaction", 3, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.maxLogFiles", "4");
        newConfig.put("aries.transaction.howl.maxBlocksPerFile", "4");
        newConfig.put("aries.transaction.howl.bufferSize", "4");
        newConfig.put("aries.transaction.howl.logFileDir", newLogDir.getAbsolutePath());

        long lm = earlierLastModified(new File(logDir, "transaction_1.log"));

        assertTrue(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertTrue("Old transaction log should exist", new File(logDir, "transaction_1.log").exists());
        assertTrue("New transaction log should exist", new File(newLogDir, "transaction_1.log").exists());
        assertNotEquals("Old transaction log should be touched (HOWL Log opened)", new File(logDir, "transaction_1.log").lastModified(), lm);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void existingTransactionLogChangedLogFileName() throws Exception {
        File logDir = new File(BASE, "existingTransactionLogChangedLogFileName");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("existingTransactionLogChangedLogFileName", "transaction", 2, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        earlierLastModified(new File(logDir, "transaction_1.log"));

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.logFileName", "megatransaction");

        assertTrue(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertFalse("Old transaction log should not exist", new File(logDir, "transaction_1.log").exists());
        assertTrue("New transaction log should exist", new File(logDir, "megatransaction_1.log").exists());
    }

    @Test
    @SuppressWarnings("unchecked")
    public void existingTransactionLogChangedLogFileNameAndLogDir() throws Exception {
        File logDir = new File(BASE, "existingTransactionLogChangedLogFileNameAndLogDir");
        File newLogDir = new File(BASE, "existingTransactionLogChangedLogFileNameAndLogDir-new");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("existingTransactionLogChangedLogFileNameAndLogDir", "transaction", 2, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        earlierLastModified(new File(logDir, "transaction_1.log"));

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.logFileName", "megatransaction");
        newConfig.put("aries.transaction.howl.logFileDir", newLogDir.getAbsolutePath());

        assertTrue(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertFalse("Old transaction log should not exist", new File(logDir, "transaction_1.log").exists());
        assertTrue("New transaction log should exist", new File(newLogDir, "megatransaction_1.log").exists());
    }

    @Test
    @SuppressWarnings("unchecked")
    public void existingTransactionLogChangedLogFileNameAndBlockSize() throws Exception {
        File logDir = new File(BASE, "existingTransactionLogChangedLogFileNameAndBlockSize");
        FileUtils.deleteDirectory(logDir);
        logDir.mkdirs();
        Hashtable<String, Object> properties = new Hashtable<String, Object>();
        HOWLLog txLog = createLog("existingTransactionLogChangedLogFileNameAndBlockSize", "transaction", 2, -1, 1, properties);
        txLog.doStart();
        transaction(txLog, 1, false);
        txLog.doStop();

        long lm = earlierLastModified(new File(logDir, "transaction_1.log"));

        Hashtable<String, Object> newConfig = (Hashtable<String, Object>) properties.clone();
        newConfig.put("aries.transaction.howl.logFileName", "megatransaction");
        newConfig.put("aries.transaction.howl.bufferSize", "4");

        assertTrue(TransactionLogUtils.copyActiveTransactions(properties, newConfig));
        assertNotEquals("Old transaction log should be touched (HOWL Log opened)", new File(logDir, "transaction_1.log").lastModified(), lm);
        assertTrue("New transaction log should exist", new File(logDir, "megatransaction_1.log").exists());
    }

    private HOWLLog createLog(String logFileDir, String logFileName,
                              int maxLogFiles, int maxBlocksPerFile, int bufferSizeInKB,
                              Dictionary<String, Object> properties) throws Exception {
        properties.put("aries.transaction.recoverable", "true");
        properties.put("aries.transaction.howl.bufferClassName", "org.objectweb.howl.log.BlockLogBuffer");
        properties.put("aries.transaction.howl.checksumEnabled", "true");
        properties.put("aries.transaction.howl.adler32Checksum", "true");
        properties.put("aries.transaction.howl.flushSleepTime", "50");
        properties.put("aries.transaction.howl.logFileExt", "log");
        properties.put("aries.transaction.howl.logFileName", logFileName);
        properties.put("aries.transaction.howl.minBuffers", "1");
        properties.put("aries.transaction.howl.maxBuffers", "0");
        properties.put("aries.transaction.howl.threadsWaitingForceThreshold", "-1");
        properties.put("aries.transaction.flushPartialBuffers", "true");
        String absoluteLogFileDir = new File(BASE, logFileDir).getAbsolutePath() + "/";
        properties.put("aries.transaction.howl.logFileDir", absoluteLogFileDir);
        properties.put("aries.transaction.howl.bufferSize", Integer.toString(bufferSizeInKB));
        properties.put("aries.transaction.howl.maxBlocksPerFile", Integer.toString(maxBlocksPerFile));
        properties.put("aries.transaction.howl.maxLogFiles", Integer.toString(maxLogFiles));

        return new HOWLLog("org.objectweb.howl.log.BlockLogBuffer", bufferSizeInKB,
                true, true, 50,
                absoluteLogFileDir, "log", logFileName,
                maxBlocksPerFile, 0, maxLogFiles, 1, -1, true, xidFactory, null);
    }

    private void transaction(HOWLLog log, int transactionBranchCount, boolean commit) throws Exception {
        Xid xid = xidFactory.createXid();
        List<TransactionBranchInfo> txBranches = new LinkedList<TransactionBranchInfo>();
        for (int b = 1; b <= transactionBranchCount; b++) {
            // TransactionImpl.enlistResource()
            Xid branchXid = xidFactory.createBranch(xid, b);
            txBranches.add(new TestTransactionBranchInfo(branchXid, String.format("res-%02d", b)));
        }

        // org.apache.geronimo.transaction.manager.TransactionImpl.internalPrepare()
        Object logMark = log.prepare(xid, txBranches);
        if (commit) {
            // org.apache.geronimo.transaction.manager.CommitTask.run()
            log.commit(xid, logMark);
        }
    }

    private static class TestTransactionBranchInfo implements TransactionBranchInfo {

        private final Xid xid;
        private final String name;

        public TestTransactionBranchInfo(Xid xid, String name) {
            this.xid = xid;
            this.name = name;
        }

        @Override
        public String getResourceName() {
            return name;
        }

        @Override
        public Xid getBranchXid() {
            return xid;
        }

    }

    private static class TestXidFactoryImpl extends XidFactoryImpl {

        private final byte[] baseId = new byte[Xid.MAXGTRIDSIZE];

        public TestXidFactoryImpl(byte[] tmId) {
            super(tmId);
            System.arraycopy(tmId, 0, baseId, 8, tmId.length);
        }

        @Override
        public Xid createXid() {
            byte[] globalId = baseId.clone();
            long id;
            synchronized (this) {
                id = count++;
            }
            insertLong(id, globalId, 0);
            return new XidImpl(globalId);
        }

        @Override
        public Xid createBranch(Xid globalId, int branch) {
            byte[] branchId = baseId.clone();
            branchId[0] = (byte) branch;
            branchId[1] = (byte) (branch >>> 8);
            branchId[2] = (byte) (branch >>> 16);
            branchId[3] = (byte) (branch >>> 24);
            insertLong(start, branchId, 4);
            return new XidImpl(globalId, branchId);
        }

    }

}
