/*
 * 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.activemq.artemis.tests.integration.journal;

import java.io.File;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.activemq.artemis.ArtemisConstants;
import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.SequentialFileFactory;
import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory;
import org.apache.activemq.artemis.core.io.mapped.MappedSequentialFileFactory;
import org.apache.activemq.artemis.core.io.nio.NIOSequentialFileFactory;
import org.apache.activemq.artemis.core.journal.LoaderCallback;
import org.apache.activemq.artemis.core.journal.PreparedTransactionInfo;
import org.apache.activemq.artemis.core.journal.RecordInfo;
import org.apache.activemq.artemis.core.journal.impl.JournalImpl;
import org.apache.activemq.artemis.jlibaio.LibaioContext;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.tests.util.SpawnedVMSupport;
import org.junit.Assert;
import org.junit.Test;

/**
 * This test spawns a remote VM, as we want to "crash" the VM right after the journal is filled with data
 */
public class ValidateTransactionHealthTest extends ActiveMQTestBase {

   private static final int OK = 10;

   @Test
   public void testAIO() throws Exception {
      internalTest("aio", getTestDir(), 10000, 100, true, true, 1);
   }

   @Test
   public void testAIOHugeTransaction() throws Exception {
      internalTest("aio", getTestDir(), 10000, 10000, true, true, 1);
   }

   @Test
   public void testAIOMultiThread() throws Exception {
      internalTest("aio", getTestDir(), 1000, 100, true, true, 10);
   }

   @Test
   public void testAIONonTransactionalNoExternalProcess() throws Exception {
      internalTest("aio", getTestDir(), 1000, 0, true, false, 10);
   }

   @Test
   public void testNIO() throws Exception {
      internalTest("nio", getTestDir(), 10000, 100, true, true, 1);
   }

   @Test
   public void testNIOHugeTransaction() throws Exception {
      internalTest("nio", getTestDir(), 10000, 10000, true, true, 1);
   }

   @Test
   public void testNIOMultiThread() throws Exception {
      internalTest("nio", getTestDir(), 1000, 100, true, true, 10);
   }

   @Test
   public void testNIONonTransactional() throws Exception {
      internalTest("nio", getTestDir(), 10000, 0, true, true, 1);
   }

   @Test
   public void testNIO2() throws Exception {
      internalTest("nio2", getTestDir(), 10000, 100, true, true, 1);
   }

   @Test
   public void testNIO2HugeTransaction() throws Exception {
      internalTest("nio2", getTestDir(), 10000, 10000, true, true, 1);
   }

   @Test
   public void testNIO2MultiThread() throws Exception {
      internalTest("nio2", getTestDir(), 1000, 100, true, true, 10);
   }

   @Test
   public void testNIO2NonTransactional() throws Exception {
      internalTest("nio2", getTestDir(), 10000, 0, true, true, 1);
   }

   @Test
   public void testMMap() throws Exception {
      internalTest("mmap", getTestDir(), 10000, 100, true, true, 1);
   }

   @Test
   public void testMMAPHugeTransaction() throws Exception {
      internalTest("mmap", getTestDir(), 10000, 10000, true, true, 1);
   }

   @Test
   public void testMMAPOMultiThread() throws Exception {
      internalTest("mmap", getTestDir(), 1000, 100, true, true, 10);
   }

   @Test
   public void testMMAPNonTransactional() throws Exception {
      internalTest("mmap", getTestDir(), 10000, 0, true, true, 1);
   }


   // Package protected ---------------------------------------------

   private void internalTest(final String type,
                             final String journalDir,
                             final long numberOfRecords,
                             final int transactionSize,
                             final boolean append,
                             final boolean externalProcess,
                             final int numberOfThreads) throws Exception {
      try {
         if (type.equals("aio") && !LibaioContext.isLoaded()) {
            // Using System.out as this output will go towards junit report
            System.out.println("AIO not found, test being ignored on this platform");
            return;
         }

         // This property could be set to false for debug purposes.
         if (append) {
            if (externalProcess) {
               Process process = SpawnedVMSupport.spawnVM(ValidateTransactionHealthTest.class.getCanonicalName(), type, journalDir, Long.toString(numberOfRecords), Integer.toString(transactionSize), Integer.toString(numberOfThreads));
               process.waitFor();
               Assert.assertEquals(ValidateTransactionHealthTest.OK, process.exitValue());
            } else {
               JournalImpl journal = ValidateTransactionHealthTest.appendData(type, journalDir, numberOfRecords, transactionSize, numberOfThreads);
               journal.stop();
            }
         }

         reload(type, journalDir, numberOfRecords, numberOfThreads);
      } finally {
         File file = new File(journalDir);
         deleteDirectory(file);
      }
   }

   private void reload(final String type,
                       final String journalDir,
                       final long numberOfRecords,
                       final int numberOfThreads) throws Exception {
      JournalImpl journal = ValidateTransactionHealthTest.createJournal(type, journalDir);

      journal.start();
      try {
         Loader loadTest = new Loader(numberOfRecords);
         journal.load(loadTest);
         Assert.assertEquals(numberOfRecords * numberOfThreads, loadTest.numberOfAdds);
         Assert.assertEquals(0, loadTest.numberOfPreparedTransactions);
         Assert.assertEquals(0, loadTest.numberOfUpdates);
         Assert.assertEquals(0, loadTest.numberOfDeletes);

         if (loadTest.ex != null) {
            throw loadTest.ex;
         }
      } finally {
         journal.stop();
      }

   }

   // Inner classes -------------------------------------------------

   static class Loader implements LoaderCallback {

      int numberOfPreparedTransactions = 0;

      int numberOfAdds = 0;

      int numberOfDeletes = 0;

      int numberOfUpdates = 0;

      long expectedRecords = 0;

      Exception ex = null;

      long lastID = 0;

      Loader(final long expectedRecords) {
         this.expectedRecords = expectedRecords;
      }

      @Override
      public void addPreparedTransaction(final PreparedTransactionInfo preparedTransaction) {
         numberOfPreparedTransactions++;

      }

      @Override
      public void addRecord(final RecordInfo info) {
         if (info.id == lastID) {
            System.out.println("id = " + info.id + " last id = " + lastID);
         }

         ByteBuffer buffer = ByteBuffer.wrap(info.data);
         long recordValue = buffer.getLong();

         if (recordValue != info.id) {
            ex = new Exception("Content not as expected (" + recordValue + " != " + info.id + ")");

         }

         lastID = info.id;
         numberOfAdds++;

      }

      @Override
      public void deleteRecord(final long id) {
         numberOfDeletes++;

      }

      @Override
      public void updateRecord(final RecordInfo info) {
         numberOfUpdates++;

      }

      @Override
      public void failedTransaction(final long transactionID,
                                    final List<RecordInfo> records,
                                    final List<RecordInfo> recordsToDelete) {
      }

   }

   // Remote part of the test =================================================================

   public static void main(final String[] args) throws Exception {

      if (args.length != 5) {
         System.err.println("Use: java -cp <classpath> " + ValidateTransactionHealthTest.class.getCanonicalName() +
                               " aio|nio|mmap <journalDirectory> <NumberOfElements> <TransactionSize> <NumberOfThreads>");
         System.exit(-1);
      }
      System.out.println("Running");
      String journalType = args[0];
      String journalDir = args[1];
      long numberOfElements = Long.parseLong(args[2]);
      int transactionSize = Integer.parseInt(args[3]);
      int numberOfThreads = Integer.parseInt(args[4]);

      try {
         ValidateTransactionHealthTest.appendData(journalType, journalDir, numberOfElements, transactionSize, numberOfThreads);

         // We don't stop the journal on the case of an external process...
         // The test is making sure that committed data can be reloaded fine...
         // i.e. commits are sync on disk as stated on the transaction.
         // The journal shouldn't leave any state impeding reloading the server
      } catch (Exception e) {
         e.printStackTrace(System.out);
         System.exit(-1);
      }

      // Simulating a crash on the system right after the data was committed.
      Runtime.getRuntime().halt(ValidateTransactionHealthTest.OK);
   }

   public static JournalImpl appendData(final String journalType,
                                        final String journalDir,
                                        final long numberOfElements,
                                        final int transactionSize,
                                        final int numberOfThreads) throws Exception {
      final JournalImpl journal = ValidateTransactionHealthTest.createJournal(journalType, journalDir);

      journal.start();
      journal.load(new LoaderCallback() {

         @Override
         public void addPreparedTransaction(final PreparedTransactionInfo preparedTransaction) {
         }

         @Override
         public void addRecord(final RecordInfo info) {
         }

         @Override
         public void deleteRecord(final long id) {
         }

         @Override
         public void updateRecord(final RecordInfo info) {
         }

         @Override
         public void failedTransaction(final long transactionID,
                                       final List<RecordInfo> records,
                                       final List<RecordInfo> recordsToDelete) {
         }
      });

      LocalThread[] threads = new LocalThread[numberOfThreads];
      final AtomicLong sequenceTransaction = new AtomicLong();

      for (int i = 0; i < numberOfThreads; i++) {
         threads[i] = new LocalThread(journal, numberOfElements, transactionSize, sequenceTransaction);
         threads[i].start();
      }

      Exception e = null;
      for (LocalThread t : threads) {
         t.join();

         if (t.e != null) {
            e = t.e;
         }
      }

      if (e != null) {
         throw e;
      }

      journal.flush();

      return journal;
   }

   public static JournalImpl createJournal(final String journalType, final String journalDir) {
      JournalImpl journal = new JournalImpl(10485760, 2, 2, 0, 0, ValidateTransactionHealthTest.getFactory(journalType, journalDir, 10485760), "journaltst", "tst", 500);
      return journal;
   }

   public static SequentialFileFactory getFactory(final String factoryType, final String directory, int fileSize) {
      if (factoryType.equals("aio")) {
         return new AIOSequentialFileFactory(new File(directory), ArtemisConstants.DEFAULT_JOURNAL_BUFFER_SIZE_AIO, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_TIMEOUT_AIO, 10, false);
      } else if (factoryType.equals("nio2")) {
         return new NIOSequentialFileFactory(new File(directory), true, 1);
      } else if (factoryType.equals("mmap")) {
         return new MappedSequentialFileFactory(new File(directory), new IOCriticalErrorListener() {
            @Override
            public void onIOException(Throwable code, String message, SequentialFile file) {
               code.printStackTrace();
            }
         }, true).chunkBytes(fileSize).overlapBytes(0);
      } else {
         return new NIOSequentialFileFactory(new File(directory), false, 1);
      }
   }

   static class LocalThread extends Thread {

      final JournalImpl journal;

      final long numberOfElements;

      final int transactionSize;

      final AtomicLong nextID;

      Exception e;

      LocalThread(final JournalImpl journal,
                  final long numberOfElements,
                  final int transactionSize,
                  final AtomicLong nextID) {
         super();
         this.journal = journal;
         this.numberOfElements = numberOfElements;
         this.transactionSize = transactionSize;
         this.nextID = nextID;
      }

      @Override
      public void run() {
         try {
            int transactionCounter = 0;

            long transactionId = nextID.incrementAndGet();

            for (long i = 0; i < numberOfElements; i++) {

               long id = nextID.incrementAndGet();

               ByteBuffer buffer = ByteBuffer.allocate(512 * 3);
               buffer.putLong(id);

               if (transactionSize != 0) {
                  journal.appendAddRecordTransactional(transactionId, id, (byte) 99, buffer.array());

                  if (++transactionCounter == transactionSize) {
                     System.out.println("Commit transaction " + transactionId);
                     journal.appendCommitRecord(transactionId, true);
                     transactionCounter = 0;
                     transactionId = nextID.incrementAndGet();
                  }
               } else {
                  journal.appendAddRecord(id, (byte) 99, buffer.array(), false);
               }
            }

            if (transactionCounter != 0) {
               journal.appendCommitRecord(transactionId, true);
            }

            if (transactionSize == 0) {
               journal.debugWait();
            }
         } catch (Exception e) {
            this.e = e;
         }

      }
   }

}
