/*
 * 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.segment;

import static org.apache.jackrabbit.oak.segment.file.FileStoreBuilder.fileStoreBuilder;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

import org.apache.jackrabbit.oak.segment.file.FileStore;
import org.apache.jackrabbit.oak.spi.blob.BlobStore;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

public class ExternalBlobReferenceTest {

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

    private FileStore fileStore;

    private BlobStore blobStore;

    @Before
    public void createFileStore() throws Exception {
        blobStore = mock(BlobStore.class);
        fileStore = fileStoreBuilder(segmentFolder.getRoot()).withBlobStore(blobStore).build();
    }

    @After
    public void destroyFileStore() {
        fileStore.close();
    }

    /**
     * The {@code SegmentWriter} should be able to write blob IDs whose length
     * is between 0 and {@code Segment.BLOB_ID_SMALL_LIMIT - 1} bytes. It should
     * be possible to correctly read the blob ID back and pass it to the {@code
     * BlobStore} to obtain information about the blob.
     * <p/>
     * This code path executes only if the written stream is {@code
     * Segment.MEDIUM_LIMIT} bytes long (or more). If the length of the stream
     * is smaller, the binary value is inlined in the segment and the {@code
     * BlobStore} is never called.
     * <p/>
     * See OAK-3105.
     */
    @Test
    public void testShortBlobId() throws Exception {
        testBlobIdWithLength(SegmentTestConstants.BLOB_ID_SMALL_LIMIT - 1);
    }

    /**
     * If the {@code BlobStore} returns a blob ID whose length is {@code
     * Segment.BLOB_ID_SMALL_LIMIT} bytes long (or more), writing the stream
     * should succeed. In this case, the blob ID is considered a long blob ID
     * and an alternate encoding is used. It should be possible to correctly
     * read the blob ID back and pass it to the {@code BlobStore} to obtain
     * information about the blob.
     * <p/>
     * This code path executes only if the written stream is {@code
     * Segment.MEDIUM_LIMIT} bytes long (or more). If the length of the stream
     * is smaller, the binary value is inlined in the segment and the {@code
     * BlobStore} is never called.
     * <p/>
     * See OAK-3105 and OAK-3107.
     */
    @Test
    public void testLongBlobId() throws Exception {
        testBlobIdWithLength(SegmentTestConstants.BLOB_ID_SMALL_LIMIT);
    }

    private void testBlobIdWithLength(int blobIdLength) throws Exception {
        String blobId = "x".repeat(blobIdLength);
        long blobLength = SegmentTestConstants.MEDIUM_LIMIT;

        doReturn(blobId).when(blobStore).writeBlob(any(InputStream.class));
        doReturn(blobLength).when(blobStore).getBlobLength(blobId);

        SegmentBlob blob = new SegmentBlob(blobStore, fileStore.getWriter().writeStream(newRandomInputStream(blobLength)));

        assertEquals(blobLength, blob.length());
    }

    private static InputStream newRandomInputStream(long size) {
        return new LimitInputStream(new ConstantInputStream(0), size);
    }

    private static class ConstantInputStream extends InputStream {

        private final int value;

        public ConstantInputStream(int value) {
            this.value = value;
        }

        @Override
        public int read() {
            return value;
        }

    }

    private static class LimitInputStream extends InputStream {

        private final InputStream stream;

        private final long limit;

        private long read = 0;

        public LimitInputStream(InputStream stream, long limit) {
            this.stream = stream;
            this.limit = limit;
        }

        @Override
        public int read() throws IOException {
            if (read >= limit) {
                return -1;
            }

            read = read + 1;

            return stream.read();
        }

    }

}
