/**
 * Copyright Microsoft Corporation
 * 
 * Licensed 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 com.microsoft.azure.storage.blob;

import com.microsoft.azure.storage.AccessCondition;
import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.ResponseReceivedEvent;
import com.microsoft.azure.storage.StorageEvent;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.TestRunners.CloudTests;
import com.microsoft.azure.storage.core.SR;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static org.junit.Assert.*;

/**
 * Blob Output Stream Tests
 */
@Category({ CloudTests.class })
public class BlobOutputStreamTests {

    protected CloudBlobContainer container;

    @Before
    public void blobOutputStreamTestMethodSetUp() throws URISyntaxException, StorageException {
        this.container = BlobTestHelper.getRandomContainerReference();
        this.container.create();
    }

    @After
    public void blobOutputStreamTestMethodTearDown() throws StorageException {
        this.container.deleteIfExists();
    }

    @Test
    public void testEmpty() throws URISyntaxException, StorageException, IOException {
        String blobName = BlobTestHelper.generateRandomBlobNameWithPrefix("testblob");

        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(blobName);
        BlobOutputStream str = blockBlob.openOutputStream();
        str.close();
        
        CloudBlockBlob blockBlob2 = this.container.getBlockBlobReference(blobName);
        blockBlob2.downloadAttributes();
        assertEquals(0, blockBlob2.getProperties().getLength());
        
        ArrayList<BlockEntry> blocks = blockBlob2.downloadBlockList(BlockListingFilter.ALL, null, null, null);
        assertEquals(0, blocks.size());
    }

    @Test
    public void testClose() throws URISyntaxException, StorageException, IOException {
        String blobName = BlobTestHelper.generateRandomBlobNameWithPrefix("testblob");

        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(blobName);
        BlobOutputStream str = blockBlob.openOutputStream();
        str.close();
        
        try {
            str.close();
            fail("Can't close twice.");
        } catch(IOException e) {
            assertEquals(SR.STREAM_CLOSED, e.getMessage());
        }
        
        str = blockBlob.openOutputStream();
        str.write(8);
        ArrayList<BlockEntry> blocks = blockBlob.downloadBlockList(BlockListingFilter.ALL, null, null, null);
        assertEquals(0, blocks.size());
        
        str.close();
        blocks = blockBlob.downloadBlockList(BlockListingFilter.COMMITTED, null, null, null);
        assertEquals(1, blocks.size());
    }

    @Test
    public void testWithAccessCondition() throws URISyntaxException, StorageException, IOException {
        int blobLengthToUse = 8 * 512;
        byte[] buffer = BlobTestHelper.getRandomBuffer(blobLengthToUse);
        String blobName = BlobTestHelper.generateRandomBlobNameWithPrefix("testblob");
        AccessCondition accessCondition = AccessCondition.generateIfNotModifiedSinceCondition(new Date()); 
        
        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(blobName);
        BlobOutputStream blobOutputStream = blockBlob.openOutputStream(accessCondition, null, null);
        ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer);
        blobOutputStream.write(inputStream, 512);

        inputStream = new ByteArrayInputStream(buffer, 512, 3 * 512);
        blobOutputStream.write(inputStream, 3 * 512);

        blobOutputStream.close();

        byte[] result = new byte[blobLengthToUse];
        blockBlob.downloadToByteArray(result, 0);

        int i = 0;
        for (; i < 4 * 512; i++) {
            assertEquals(buffer[i], result[i]);
        }

        for (; i < 8 * 512; i++) {
            assertEquals(0, result[i]);
        }
    }

    @Test
    public void testWriteStream() throws URISyntaxException, StorageException, IOException {
        int blobLengthToUse = 8 * 512;
        byte[] buffer = BlobTestHelper.getRandomBuffer(blobLengthToUse);
        String blobName = BlobTestHelper.generateRandomBlobNameWithPrefix("testblob");

        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(blobName);
        BlobOutputStream blobOutputStream = blockBlob.openOutputStream();
        ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer);
        blobOutputStream.write(inputStream, 512);

        inputStream = new ByteArrayInputStream(buffer, 512, 3 * 512);
        blobOutputStream.write(inputStream, 3 * 512);

        blobOutputStream.close();

        byte[] result = new byte[blobLengthToUse];
        blockBlob.downloadToByteArray(result, 0);

        int i = 0;
        for (; i < 4 * 512; i++) {
            assertEquals(buffer[i], result[i]);
        }

        for (; i < 8 * 512; i++) {
            assertEquals(0, result[i]);
        }
    }

    @Test
    public void testFlush() throws Exception {
        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(
                BlobTestHelper.generateRandomBlobNameWithPrefix("flush"));
        
        OperationContext ctx = new OperationContext();
        ctx.getResponseReceivedEventHandler().addListener(new StorageEvent<ResponseReceivedEvent>() {
            
            @Override
            public void eventOccurred(ResponseReceivedEvent eventArg) {
                try {
                    HttpURLConnection con = (HttpURLConnection) eventArg.getConnectionObject();
                    if ("511".equals(con.getRequestProperty(Constants.HeaderConstants.CONTENT_LENGTH))) {
                        Thread.sleep(3000);   
                    }
                } catch (InterruptedException e) {
                    // do nothing
                }
            }
        });
        
        BlobOutputStream blobOutputStream = blockBlob.openOutputStream(null, null, ctx);
        
        ExecutorService threadExecutor = Executors.newFixedThreadPool(1);
        
        byte[] buffer = BlobTestHelper.getRandomBuffer(511);
        blobOutputStream.write(buffer);
        
        Future<Void> future = threadExecutor.submit(new FlushTask(blobOutputStream));
        Thread.sleep(1000);
        
        buffer = BlobTestHelper.getRandomBuffer(513);
        blobOutputStream.write(buffer);
        
        // Writes complete when the upload is dispatched (not when the upload completes and flush must
        // wait for upload1 to complete. So, flush should finish last and writes should finish in order.
        while(!future.isDone()) {
            Thread.sleep(500);
        }
        
        // After flush we should see the first upload
        ArrayList<BlockEntry> blocks = blockBlob.downloadBlockList(BlockListingFilter.UNCOMMITTED, null, null, null);
        assertEquals(1, blocks.size());
        assertEquals(511, blocks.get(0).getSize());
        
        // After close we should see the second upload
        blobOutputStream.close();
        blocks = blockBlob.downloadBlockList(BlockListingFilter.COMMITTED, null, null, null);
        assertEquals(2, blocks.size());
        assertEquals(513, blocks.get(1).getSize());
    }

    @Test
    public void testWritesDoubleConcurrency() throws URISyntaxException, StorageException, IOException,
            InterruptedException {
        String blobName = BlobTestHelper.generateRandomBlobNameWithPrefix("concurrency");
        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(blobName);

        // setup the blob output stream with a concurrency of 5
        BlobRequestOptions options = new BlobRequestOptions();
        options.setConcurrentRequestCount(5);
        BlobOutputStream blobOutputStream = blockBlob.openOutputStream(null, options, null);

        // set up the execution completion service
        ExecutorService threadExecutor = Executors.newFixedThreadPool(5);
        ExecutorCompletionService<Void> completion = new ExecutorCompletionService<Void>(threadExecutor);
        
        int tasks = 10;
        int writes = 10;
        int length = 512;
        
        // submit tasks to write and flush many blocks
        for (int i = 0; i < tasks; i++) {
            completion.submit(new WriteTask(blobOutputStream, length, writes, 4 /*flush period*/));
        }

        // wait for all tasks to complete
        for (int i = 0; i < tasks; i++) {
            completion.take();
        }

        // shut down the thread executor for this method
        threadExecutor.shutdown();

        // check that blocks were committed
        ArrayList<BlockEntry> blocks = blockBlob.downloadBlockList(BlockListingFilter.UNCOMMITTED, null, null, null);
        assertTrue(blocks.size() != 0);
        
        // close the stream and check that the blob is the expected length
        blobOutputStream.close();
        blockBlob.downloadAttributes();
        assertTrue(blockBlob.getProperties().getLength() == length*writes*tasks);
    }

    @Test
    public void testWritesNoConcurrency() throws URISyntaxException, StorageException, IOException {
        int writes = 10;
        
        this.smallPutThresholdHelper(Constants.MB, writes, null);
        this.writeFlushHelper(512, writes, null, 1);
        this.writeFlushHelper(512, writes, null, 4);
        this.writeFlushHelper(512, writes, null, writes+1);
    }
    
    public void testWritesConcurrency() throws URISyntaxException, StorageException, IOException {
        int writes = 10;
        
        BlobRequestOptions options = new BlobRequestOptions();
        options.setConcurrentRequestCount(5); 
        
        this.smallPutThresholdHelper(Constants.MB, writes, options);
        this.writeFlushHelper(512, writes, options, 1);
        this.writeFlushHelper(512, writes, options, 4);
        this.writeFlushHelper(512, writes, options, writes+1);
    }
    
    private void smallPutThresholdHelper(int length, int writes, BlobRequestOptions options) 
            throws URISyntaxException, StorageException, IOException {    
        byte[] buffer = BlobTestHelper.getRandomBuffer(length*writes);
        
        String blobName = BlobTestHelper.generateRandomBlobNameWithPrefix("concurrency");
        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(blobName);
        blockBlob.setStreamWriteSizeInBytes(length);
              
        BlobOutputStream blobOutputStream = blockBlob.openOutputStream(null, options, null);      
        for (int i = 0; i < writes; i ++) {
            blobOutputStream.write(buffer, i*length, length);
        }
        
        blobOutputStream.flush();
        ArrayList<BlockEntry> blocks = blockBlob.downloadBlockList(BlockListingFilter.UNCOMMITTED, null, null, null);
        assertEquals(writes, blocks.size());
        
        blobOutputStream.close();
        blocks = blockBlob.downloadBlockList(BlockListingFilter.COMMITTED, null, null, null);
        assertEquals(writes, blocks.size());
        
        byte[] outBuffer = new byte[writes*length];
        blockBlob.downloadToByteArray(outBuffer, 0);
        for (int i = 0; i < length*writes; i ++) {
            assertEquals(buffer[i], outBuffer[i]);
        }
    }
    
    private void writeFlushHelper(int length, int writes, BlobRequestOptions options, int flushPeriod)
            throws URISyntaxException, StorageException, IOException {
        byte[] buffer = BlobTestHelper.getRandomBuffer(length*writes);
        
        String blobName = BlobTestHelper.generateRandomBlobNameWithPrefix("concurrency");
        CloudBlockBlob blockBlob = this.container.getBlockBlobReference(blobName);
        
        ArrayList<BlockEntry> blocks;
        BlobOutputStream blobOutputStream = blockBlob.openOutputStream(null, options, null);      
        for (int i = 0; i < writes; i ++) {
            blobOutputStream.write(buffer, i*length, length);
            
            if ((i+1)%flushPeriod == 0) {
                blobOutputStream.flush();
                blocks = blockBlob.downloadBlockList(BlockListingFilter.UNCOMMITTED, null, null, null);
                assertEquals((int)Math.ceil((i+1)/flushPeriod), blocks.size());   
            }
        }
        
        blobOutputStream.close();
        blocks = blockBlob.downloadBlockList(BlockListingFilter.COMMITTED, null, null, null);
        
        int flushRequired = writes-flushPeriod < 0 || writes%flushPeriod == 0 ? 0 : 1;
        double expected = Math.ceil(((double)writes+flushRequired)/flushPeriod);
        assertEquals((long)expected, blocks.size());
        
        byte[] outBuffer = new byte[writes*length];
        blockBlob.downloadToByteArray(outBuffer, 0);
        for (int i = 0; i < length*writes; i ++) {
            assertEquals(buffer[i], outBuffer[i]);
        }
    }
    
    private static class FlushTask implements Callable<Void> {
        final BlobOutputStream stream;
        
        public FlushTask(BlobOutputStream stream) {
            this.stream = stream;
        }
        
        @Override
        public Void call() {
            try {
                stream.flush();
            } catch (IOException e) {
                fail("The flush should succeed.");
            }
            return null;
        }
    }
    
    private class WriteTask implements Callable<Void> {
        final int length;
        final int writes;
        final int flushPeriod;
        final BlobOutputStream blobOutputStream;
        
        public WriteTask(BlobOutputStream blobOutputStream, int length, int writes, int flushPeriod) {
            this.length = length;
            this.writes = writes;
            this.flushPeriod = flushPeriod;
            this.blobOutputStream = blobOutputStream;
        }
        
        @Override
        public Void call() {
            try {
                byte[] buffer = BlobTestHelper.getRandomBuffer(this.length*this.writes);
                for (int i = 0; i < writes; i ++) {
                    this.blobOutputStream.write(buffer, i*this.length, this.length);
                    
                    if ((i+1)%flushPeriod == 0) {
                        this.blobOutputStream.flush();
                    }
                }
            } catch (Exception e) {
                fail("flushHelper should succeed.");
            }
            return null;
        }
    }
}