/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.exoplatform.services.jcr.impl.storage.value.fs;

import org.exoplatform.services.jcr.JcrImplBaseTest;
import org.exoplatform.services.jcr.datamodel.ValueData;
import org.exoplatform.services.jcr.impl.dataflow.persistent.StreamPersistedValueData;
import org.exoplatform.services.jcr.impl.storage.value.cas.RecordAlreadyExistsException;
import org.exoplatform.services.jcr.impl.storage.value.cas.RecordNotFoundException;
import org.exoplatform.services.jcr.impl.storage.value.cas.ValueContentAddressStorage;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * Created by The eXo Platform SAS
 * 
 * Date: 19.07.2008
 * 
 * @author <a href="mailto:peter.nedonosko@exoplatform.com.ua">Peter Nedonosko</a>
 * @version $Id$
 */
public abstract class CASableFileIOChannelTestBase extends JcrImplBaseTest
{//

   private static Log LOG = ExoLogger.getLogger("exo.jcr.component.core.CASableFileIOChannelTestBase");

   protected ValueContentAddressStorage vcas;

   protected File rootDir;

   protected String storageId;

   protected File testFile;

   @Override
   public void setUp() throws Exception
   {
      super.setUp();

      if (vcas == null)
         initVCAS();

      if (rootDir == null)
      {
         rootDir = new File("target/temp/values-test");
         rootDir.mkdirs();

         new File(rootDir, FileValueStorage.TEMP_DIR_NAME).mkdirs();

         if (!rootDir.exists())
            throw new Exception("Folder does not exist " + rootDir.getAbsolutePath());
      }

      if (storageId == null)
         storageId = "#1";

      if (testFile == null)
         testFile = createBLOBTempFile(2048); // 2M
   }

   @Override
   protected void tearDown() throws Exception
   {
      // clean rootDir
      deleteRecursive(rootDir);
      rootDir = null;

      super.tearDown();
   }

   protected abstract void initVCAS() throws Exception;

   protected abstract FileIOChannel openCASChannel(String digestType) throws Exception;

   /**
    * Write value in channel. Check if storage contains appropriate file.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void write(String digestType) throws Exception
   {
      FileIOChannel fch = openCASChannel(digestType);

      String propertyId = IdGenerator.generate();
      ValueData value = new StreamPersistedValueData(0, new FileInputStream(testFile));

      fch.write(propertyId, value);
      fch.commit();

      File vsfile =
         new File(rootDir, fch.makeFilePath(vcas.getIdentifier(propertyId, 0), CASableIOSupport.HASHFILE_ORDERNUMBER)); // orderNum
      // =0
      assertTrue("File should exists " + vsfile.getAbsolutePath(), vsfile.exists());

      InputStream etalon, tested;
      compareStream(etalon = new FileInputStream(testFile), tested = new FileInputStream(vsfile));
      etalon.close();
      tested.close();
   }

   /**
    * Tries write value already existed in channel.
    * 
    * Check if excpetion RecordAlreadyExistsException will be thrown and storage content will not be
    * changed.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeExisting(String digestType) throws Exception
   {
      FileIOChannel fch = openCASChannel(digestType);

      // prepare
      String propertyId = IdGenerator.generate();
      ValueData value = new StreamPersistedValueData(0, new FileInputStream(testFile));
      fch.write(propertyId, value);
      fch.commit();

      long initialSize = calcDirSize(rootDir);

      try
      {
         fch = openCASChannel(digestType);
         fch.write(new String(propertyId), new StreamPersistedValueData(0, new FileInputStream(testFile)));
         fch.commit();

         fail("RecordAlreadyExistsException should be thrown, record exists");
      }
      catch (RecordAlreadyExistsException e)
      {
         // ok
      }

      assertEquals("Storage size must be unchanged ", initialSize, calcDirSize(rootDir));
   }

   /**
    * Tries update (delete/write) just added value in this transaction.
    * 
    * Check if excpetion RecordAlreadyExistsException will be thrown and storage content will not be
    * changed.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeDeleteWriteInSameTransaction(String digestType) throws Exception
   {
      FileIOChannel fch = openCASChannel(digestType);

      // prepare
      String propertyId = IdGenerator.generate();
      try
      {
         ValueData value = new StreamPersistedValueData(0, new FileInputStream(testFile));
         fch.write(propertyId, value);
         fch.delete(propertyId);
         fch.write(propertyId, new StreamPersistedValueData(0, new FileInputStream(testFile)));
         fch.commit();

         // long initialSize = calcDirSize(rootDir);
      }
      catch (RecordAlreadyExistsException e)
      {
         fail("RecordAlreadyExistsException should not be thrown, record updated in same transaction");
      }

      // assertEquals("Storage size must be unchanged ", initialSize, calcDirSize(rootDir));
   }

   /**
    * Write and read value in channel. Check if storage contains value equals to the given.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeRead(String digestType) throws Exception
   {
      FileIOChannel fch = openCASChannel(digestType);

      String propertyId = IdGenerator.generate();
      ValueData value = new StreamPersistedValueData(0, new FileInputStream(testFile));
      fch.write(propertyId, value);
      fch.commit();

      ValueData fvalue = fch.read(propertyId, value.getOrderNumber(), 200 * 1024);

      InputStream etalon, tested;
      compareStream(etalon = new FileInputStream(testFile), tested = fvalue.getAsStream());
      etalon.close();
      tested.close();
   }

   /**
    * Write and delete value in channel. Checks if value is deleted.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeDelete(String digestType) throws Exception
   {
      FileIOChannel fch = openCASChannel(digestType);

      String propertyId = IdGenerator.generate();
      ValueData value = new StreamPersistedValueData(0, new FileInputStream(testFile));
      fch.write(propertyId, value);
      fch.commit();

      File vsfile =
         new File(rootDir, fch.makeFilePath(vcas.getIdentifier(propertyId, 0), CASableIOSupport.HASHFILE_ORDERNUMBER)); // orderNum
      // =0

      fch.delete(propertyId);
      fch.commit();

      assertFalse("File should not exists " + vsfile.getAbsolutePath(), vsfile.exists());
   }

   /**
    * Tries delete not existing value in channel.
    * 
    * Check if excpetion RecordNotFoundException will be thrown and storage content will not be
    * changed.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void deleteNotExisting(String digestType) throws Exception
   {
      long initialSize = calcDirSize(rootDir);

      try
      {
         FileIOChannel fch = openCASChannel(digestType);
         fch.delete(IdGenerator.generate());
         fch.commit();
         fail("RecordNotFoundException should be thrown, record not found");
      }
      catch (RecordNotFoundException e)
      {
         // ok
      }

      assertEquals("Storage size must be unchanged ", initialSize, calcDirSize(rootDir));
   }

   /**
    * Tries read not existing value in channel.<br/>
    * 
    * Check if excpetion RecordNotFoundException will be thrown and storage content will not be
    * changed.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void readNotExisting(String digestType) throws Exception
   {
      long initialSize = calcDirSize(rootDir);

      try
      {
         openCASChannel(digestType).read(IdGenerator.generate(), 1, 1024 * 200);
         fail("RecordNotFoundException should be thrown, record not found");
      }
      catch (RecordNotFoundException e)
      {
         // ok
      }

      assertEquals("Storage size must be unchanged ", initialSize, calcDirSize(rootDir));
   }

   /**
    * Write multivalued property with same content address. Check if storage contains only one file.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeSameMultivalued(String digestType) throws Exception
   {
      FileIOChannel fch = openCASChannel(digestType);

      String propertyId = IdGenerator.generate();

      long initialSize = calcDirSize(rootDir);

      for (int i = 0; i < 20; i++)
      {
         fch.write(propertyId, new StreamPersistedValueData(i, new FileInputStream(testFile)));
      }
      fch.commit();

      File vsfile =
         new File(rootDir, fch.makeFilePath(vcas.getIdentifier(propertyId, 15), CASableIOSupport.HASHFILE_ORDERNUMBER));
      assertTrue("File should exists " + vsfile.getAbsolutePath(), vsfile.exists());

      assertEquals("Storage size must be increased on size of ONE file ", initialSize + testFile.length(),
         calcDirSize(rootDir));
   }

   /**
    * Write multivalued property with unique content address. Check if storage contains all files.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeUniqueMultivalued(String digestType) throws Exception
   {
      FileIOChannel fch = openCASChannel(digestType);

      String propertyId = IdGenerator.generate();

      long initialSize = calcDirSize(rootDir);
      long addedSize = 0;
      for (int i = 0; i < 20; i++)
      {
         File f = createBLOBTempFile(300);
         addedSize += f.length();
         fch.write(propertyId, new StreamPersistedValueData(i, new FileInputStream(f)));
      }
      fch.commit();

      File vsfile =
         new File(rootDir, fch.makeFilePath(vcas.getIdentifier(propertyId, 15), CASableIOSupport.HASHFILE_ORDERNUMBER));
      assertTrue("File should exists " + vsfile.getAbsolutePath(), vsfile.exists());

      assertEquals("Storage size must be increased on size of ALL files ", initialSize + addedSize,
         calcDirSize(rootDir));
   }

   /**
    * Write set of properties with same content address. Check if storage contains only one file.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeSameProperties(String digestType) throws Exception
   {
      long initialSize = calcDirSize(rootDir);

      String propertyId = null;
      final int count = 20;
      for (int i = 0; i < count; i++)
      {
         propertyId = IdGenerator.generate();

         FileIOChannel fch = openCASChannel(digestType);
         fch.write(propertyId, new StreamPersistedValueData(0, new FileInputStream(testFile)));
         fch.commit();
      }

      assertEquals("Storage size must be increased on size of ONE file ", initialSize + testFile.length(),
         calcDirSize(rootDir));
   }

   /**
    * Write set of properties with unique content address. Check if storage contains all file.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void writeUniqueProperties(String digestType) throws Exception
   {
      long initialSize = calcDirSize(rootDir);
      long addedSize = 0;

      String propertyId = null;
      final int count = 20;
      for (int i = 0; i < count; i++)
      {
         propertyId = IdGenerator.generate();

         File f = createBLOBTempFile(300);
         addedSize += f.length();

         FileIOChannel fch = openCASChannel(digestType);
         fch.write(propertyId, new StreamPersistedValueData(i, new FileInputStream(f)));
         fch.commit();
      }

      assertEquals("Storage size must be increased on size of ALL files ", initialSize + addedSize,
         calcDirSize(rootDir));
   }

   /**
    * Delete one of properties with same content address. Check if storage still contains (only one)
    * file.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void deleteSameProperty(String digestType) throws Exception
   {
      long initialSize = calcDirSize(rootDir);

      // add some files
      String propertyId = null;
      final int count = 20;
      for (int i = 0; i < count; i++)
      {

         String pid = IdGenerator.generate();
         if (i == Math.round(count / 2))
            propertyId = pid;

         FileIOChannel fch = openCASChannel(digestType);
         fch.write(pid, new StreamPersistedValueData(0, new FileInputStream(testFile)));
         fch.commit();
      }

      // remove mapping in VCAS for one of files
      FileIOChannel fch = openCASChannel(digestType);
      fch.delete(propertyId);
      fch.commit();

      assertEquals("Storage size must be unchanged after the delete ", initialSize + testFile.length(),
         calcDirSize(rootDir));
   }

   /**
    * Delete one of properties with unique content address. Check if storage contains on one file
    * less.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void deleteUniqueProperty(String digestType) throws Exception
   {
      long initialSize = calcDirSize(rootDir);

      // add some files
      String propertyId = null;
      final int count = 20;
      final int fileSizeKb = 355;
      long fileSize = 0;
      long addedSize = 0;

      for (int i = 0; i < count; i++)
      {
         String pid = IdGenerator.generate();
         if (i == Math.round(count / 2))
            propertyId = pid;

         File f = createBLOBTempFile(fileSizeKb);
         addedSize += (fileSize = f.length());

         FileIOChannel fch = openCASChannel(digestType);
         fch.write(pid, new StreamPersistedValueData(i, new FileInputStream(f)));
         fch.commit();
      }

      // remove mapping in VCAS for one of files
      FileIOChannel fch = openCASChannel(digestType);
      fch.delete(propertyId);
      fch.commit();

      assertEquals("Storage size must be decreased on one file size after the delete ", initialSize
         + (addedSize - fileSize), calcDirSize(rootDir));
   }

   /**
    * Delete one of properties with value shared between some values in few properties.
    * 
    * Check if storage contains only files related to the values.
    * 
    * @param digestType
    * @throws Exception
    */
   protected void addDeleteSharedMultivalued(String digestType) throws Exception
   {
      long initialSize = calcDirSize(rootDir);

      FileIOChannel fch = openCASChannel(digestType);

      final String property1MultivaluedId = IdGenerator.generate();

      StreamPersistedValueData sharedValue = null;

      // add multivaued property
      long m1fileSize = 0;
      long m1filesCount = 0;
      long addedSize = 0;
      for (int i = 0; i < 5; i++)
      {
         File f = createBLOBTempFile(450);
         addedSize += (m1fileSize = f.length());

         StreamPersistedValueData v = new StreamPersistedValueData(i, new FileInputStream(f));

         if (i == 1)
            sharedValue = v;
         else
            m1filesCount++;

         fch.write(property1MultivaluedId, v);
      }
      fch.commit();

      // add another multivalued with shared file
      final String property2MultivaluedId = IdGenerator.generate();
      long m2fileSize = 0;
      long m2filesCount = 0;
      fch = openCASChannel(digestType);
      for (int i = 0; i < 4; i++)
      {
         ValueData v;
         if (i == 2)
         {
            // use shared
            sharedValue = new StreamPersistedValueData(i, sharedValue.getAsStream());
            v = sharedValue;
         }
         else
         {
            // new file
            m2filesCount++;
            File f = createBLOBTempFile(350);
            addedSize += (m2fileSize = f.length()); // add size
            v = new StreamPersistedValueData(i, new FileInputStream(f));
         }
         fch.write(property2MultivaluedId, v);
      }
      fch.commit();

      // add some single valued properties, two new property will have shared value too
      String property1Id = null;
      String property2Id = null;
      sharedValue = new StreamPersistedValueData(0, sharedValue.getAsStream());
      for (int i = 0; i < 10; i++)
      {
         String pid = IdGenerator.generate();
         ValueData v;
         if (i == 1)
         {
            property1Id = pid;
            v = sharedValue;
         }
         else if (i == 5)
         {
            property2Id = pid;
            v = sharedValue;
         }
         else
         {
            File f = createBLOBTempFile(425);
            addedSize += f.length();
            v = new StreamPersistedValueData(i, new FileInputStream(f));
         }
         FileIOChannel vfch = openCASChannel(digestType);
         vfch.write(pid, v);
         vfch.commit();
      }

      // final size
      long finalSize = initialSize + addedSize;

      // remove mapping in VCAS for singlevalued property #2
      fch = openCASChannel(digestType);
      fch.delete(property2Id);
      fch.commit();
      assertEquals("Storage size must be unchanged after the delete of property #2 ", finalSize, calcDirSize(rootDir));

      // remove mapping in VCAS for multivalued property #1
      finalSize -= m1fileSize * m1filesCount;
      fch = openCASChannel(digestType);
      fch.delete(property1MultivaluedId);
      fch.commit();
      assertEquals("Storage size must be unchanged after the delete of multivalue property #1 ", finalSize,
         calcDirSize(rootDir));

      // remove mapping in VCAS for multivalued property #2
      finalSize -= m2fileSize * m2filesCount;
      fch = openCASChannel(digestType);
      fch.delete(property2MultivaluedId);
      fch.commit();
      assertEquals("Storage size must be decreased on " + (m2fileSize * m2filesCount)
         + " bytes after the delete of multivalue property #2 ", finalSize, calcDirSize(rootDir));

      // remove mapping in VCAS for singlevalued property #1
      finalSize -= m1fileSize;
      fch = openCASChannel(digestType);
      fch.delete(property1Id);
      fch.commit();
      assertEquals("Storage size must be decreased on " + m1fileSize + " bytes after the delete of property #1 ",
         finalSize, calcDirSize(rootDir));
   }

   // ----- utilities -----

   private long deleteRecursive(File dir)
   {
      long count = 0;
      for (File sf : dir.listFiles())
      {
         if (sf.isDirectory() && sf.list().length > 0)
            count += deleteRecursive(sf);
         else if (sf.delete())
            count += 1;
         else
            LOG.warn("Can't delete file " + sf.getAbsolutePath());
      }
      count += dir.delete() ? 1 : 0;
      return count;
   }

   private long calcDirSize(File dir)
   {
      long size = 0;
      for (File sf : dir.listFiles())
      {
         if (sf.isDirectory())
            size += calcDirSize(sf);
         else
            size += sf.length();
      }
      return size;
   }

   // ------ tests ------

   // public void testWriteDeleteWriteMD5() throws Exception {
   // writeDeleteWriteInSameTransaction("MD5");
   // }
   //
   // public void testWriteDeleteWriteSHA1() throws Exception {
   // writeDeleteWriteInSameTransaction("SHA1");
   // }

   public void testWriteMD5() throws Exception
   {
      write("MD5");
   }

   public void testWriteSHA1() throws Exception
   {
      write("SHA1");
   }

   public void testReadMD5() throws Exception
   {
      writeRead("MD5");
   }

   public void testReadSHA1() throws Exception
   {
      writeRead("SHA1");
   }

   public void testDeleteMD5() throws Exception
   {
      writeDelete("MD5");
   }

   public void testDeleteSHA1() throws Exception
   {
      writeDelete("SHA1");
   }

   public void testMultivaluedMD5() throws Exception
   {
      writeSameMultivalued("MD5");
   }

   public void testMultivaluedSHA1() throws Exception
   {
      writeSameMultivalued("SHA1");
   }

   public void testUniqueMultivaluedMD5() throws Exception
   {
      writeUniqueMultivalued("MD5");
   }

   public void testUniqueMultivaluedSHA1() throws Exception
   {
      writeUniqueMultivalued("SHA1");
   }

   public void testSamePropertiesMD5() throws Exception
   {
      writeSameProperties("MD5");
   }

   public void testSamePropertiesSHA1() throws Exception
   {
      writeSameProperties("SHA1");
   }

   public void testUniquePropertiesMD5() throws Exception
   {
      writeUniqueProperties("MD5");
   }

   public void testUniquePropertiesSHA1() throws Exception
   {
      writeUniqueProperties("SHA1");
   }

   public void testDeleteSamePropertyMD5() throws Exception
   {
      deleteSameProperty("MD5");
   }

   public void testDeleteSamePropertySHA1() throws Exception
   {
      deleteSameProperty("SHA1");
   }

   public void testDeleteUniquePropertyMD5() throws Exception
   {
      deleteUniqueProperty("MD5");
   }

   public void testDeleteUniquePropertySHA1() throws Exception
   {
      deleteUniqueProperty("SHA1");
   }

   public void testAddDeleteSharedMultivaluedMD5() throws Exception
   {
      addDeleteSharedMultivalued("MD5");
   }

   public void testAddDeleteSharedMultivaluedSHA1() throws Exception
   {
      addDeleteSharedMultivalued("SHA1");
   }

   // failt tests

   public void testAddExistingMD5() throws Exception
   {
      writeExisting("MD5");
   }

   public void testAddExistingSHA1() throws Exception
   {
      writeExisting("SHA1");
   }

   public void testRemoveNotExistingMD5() throws Exception
   {
      deleteNotExisting("MD5");
   }

   public void testRemoveNotExistingSHA1() throws Exception
   {
      deleteNotExisting("SHA1");
   }

   public void testReadNotExistingMD5() throws Exception
   {
      readNotExisting("MD5");
   }

   public void testReadNotExistingSHA1() throws Exception
   {
      readNotExisting("SHA1");
   }

}
