/*
 * 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.jackrabbit.oak.upgrade.blob;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Properties;
import java.util.Random;

import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import org.apache.jackrabbit.core.data.DataIdentifier;
import org.apache.jackrabbit.core.data.DataRecord;
import org.apache.jackrabbit.core.data.DataStoreException;
import org.apache.jackrabbit.core.data.FileDataStore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

public class LengthCachingDataStoreTest {

    @Rule
    public final TemporaryFolder tempFolder = new TemporaryFolder(new File("target"));

    @Test
    public void mappingFileData() throws Exception {
        File root = tempFolder.getRoot();
        File mappingFile = new File(root, "mapping.txt");
        String text = "1000|foo\n2000|bar";
        Files.write(text, mappingFile, Charset.defaultCharset());

        LengthCachingDataStore fds = new LengthCachingDataStore();
        fds.setDelegateClass(FileDataStore.class.getName());
        fds.setMappingFilePath(mappingFile.getAbsolutePath());
        fds.init(tempFolder.getRoot().getAbsolutePath());

        DataRecord dr = fds.getRecord(new DataIdentifier("foo"));
        assertNotNull(dr);
        assertEquals(1000, dr.getLength());

        assertEquals(2000, fds.getRecord(new DataIdentifier("bar")).getLength());
    }

    @Test
    public void configDelegate() throws Exception{
        //1. Store the config in a file
        Properties p = new Properties();
        p.setProperty("minRecordLength", "4972");
        File configFile = tempFolder.newFile();
        FileOutputStream fos = new FileOutputStream(configFile);
        p.store(fos, null);
        fos.close();

        //2. Configure the delegate and config file
        LengthCachingDataStore fds = new LengthCachingDataStore();
        fds.setDelegateClass(FileDataStore.class.getName());
        fds.setDelegateConfigFilePath(configFile.getAbsolutePath());
        fds.init(tempFolder.getRoot().getAbsolutePath());

        assertEquals(4972, fds.getMinRecordLength());
    }

    @Test
    public void delegateRecordTest() throws Exception{
        FileDataStore ds = new FileDataStore();
        byte[] data = bytes(ds.getMinRecordLength() + 10);
        ds.init(tempFolder.getRoot().getAbsolutePath());
        DataRecord dr = ds.addRecord(new ByteArrayInputStream(data));

        File mappingFile = new File(tempFolder.getRoot(), "mapping.txt");
        String text = String.format("%s|%s", data.length, dr.getIdentifier().toString());
        Files.write(text, mappingFile, Charset.defaultCharset());

        LengthCachingDataStore fds = new LengthCachingDataStore();
        fds.setDelegateClass(FileDataStore.class.getName());
        fds.setMappingFilePath(mappingFile.getAbsolutePath());
        fds.init(tempFolder.getRoot().getAbsolutePath());

        DataRecord dr2 = fds.getRecordIfStored(dr.getIdentifier());
        assertEquals(dr, dr2);

        assertEquals(dr.getLength(), dr2.getLength());
        assertTrue(supplier(dr).contentEquals(supplier(dr2)));
    }

    @Test
    public void writeBackNewEntries() throws Exception{
        //1. Add some entries to FDS
        FileDataStore fds1 = new FileDataStore();
        File fds1Dir = tempFolder.newFolder();
        int minSize = fds1.getMinRecordLength();
        fds1.init(fds1Dir.getAbsolutePath());
        DataRecord dr1 = fds1.addRecord(byteStream(minSize + 10));
        DataRecord dr2 = fds1.addRecord(byteStream(minSize + 100));

        //2. Try reading them so as to populate the new mappings
        LengthCachingDataStore fds2 = new LengthCachingDataStore();
        fds2.setDelegateClass(FileDataStore.class.getName());
        fds2.init(fds1Dir.getAbsolutePath());

        fds2.getRecord(new DataIdentifier(dr1.getIdentifier().toString()));
        fds2.getRecord(new DataIdentifier(dr2.getIdentifier().toString()));

        File mappingFile = fds2.getMappingFile();

        //3. Get the mappings pushed to file
        fds2.close();

        //4. Open a new FDS pointing to new directory. Read should still work fine
        //as they would be served by the mapping data
        LengthCachingDataStore fds3 = new LengthCachingDataStore();
        fds3.setDelegateClass(FileDataStore.class.getName());
        fds3.setMappingFilePath(mappingFile.getAbsolutePath());
        fds3.init(tempFolder.newFolder().getAbsolutePath());
        fds3.setReadOnly(false);

        assertEquals(dr1.getLength(), fds3.getRecord(dr1.getIdentifier()).getLength());
        assertEquals(dr2.getLength(), fds3.getRecord(dr2.getIdentifier()).getLength());

        DataRecord dr3 = fds3.addRecord(byteStream(minSize + 200));
        //5. Close again so see if update of existing file works
        fds3.close();

        LengthCachingDataStore fds4 = new LengthCachingDataStore();
        fds4.setDelegateClass(FileDataStore.class.getName());
        fds4.setMappingFilePath(mappingFile.getAbsolutePath());
        fds4.init(tempFolder.newFolder().getAbsolutePath());

        assertEquals(dr3.getLength(), fds4.getRecord(dr3.getIdentifier()).getLength());
        assertEquals(dr2.getLength(), fds4.getRecord(dr2.getIdentifier()).getLength());


    }

    @Test
    public void referenceHandling() throws Exception{
        int minSize =  new FileDataStore().getMinRecordLength();
        LengthCachingDataStore fds = new LengthCachingDataStore();
        fds.setDelegateClass(FileDataStore.class.getName());
        fds.init(tempFolder.newFolder().getAbsolutePath());
        fds.setReadOnly(false);

        DataRecord dr1 = fds.addRecord(byteStream(minSize + 10));
        assertNotNull(fds.getRecordFromReference(dr1.getReference()));
        assertEquals(dr1.getIdentifier(), fds.getRecordFromReference(dr1.getReference()).getIdentifier());
    }

    private InputStream byteStream(int size) {
        return new ByteArrayInputStream(bytes(size));
    }

    private byte[] bytes(int size) {
        byte[] data = new byte[size];
        new Random().nextBytes(data);
        return data;
    }

    private static ByteSource supplier(final DataRecord dr) {
        return new ByteSource() {
            @Override
            public InputStream openStream() throws IOException {
                try {
                    return dr.getStream();
                } catch (DataStoreException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }
}
