/*
 * Copyright (c) 2008-2014 MongoDB, Inc.
 *
 * 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.mongodb.gridfs;

import category.Slow;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DatabaseTestCase;
import com.mongodb.MongoException;
import org.bson.types.ObjectId;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Scanner;

import static java.nio.charset.Charset.defaultCharset;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class GridFSTest extends DatabaseTestCase {
    private GridFS gridFS;

    @Before
    public void setUp() {
        super.setUp();
        gridFS = new GridFS(database);
    }

    @Test
    public void testSmall() throws Exception {
        testInOut("this is a simple test");
    }

    @Test
    public void testBig() throws Exception {
        int target = GridFS.DEFAULT_CHUNKSIZE * 3;
        StringBuilder buf = new StringBuilder(target);
        while (buf.length() < target) {
            buf.append("asdasdkjasldkjasldjlasjdlajsdljasldjlasjdlkasjdlaskjdlaskjdlsakjdlaskjdasldjsad");
        }
        String s = buf.toString();
        testInOut(s);
    }

    void testOutStream(final String s) throws Exception {

        int[] start = getCurrentCollectionCounts();

        GridFSInputFile in = gridFS.createFile();
        OutputStream writeStream = in.getOutputStream();
        writeStream.write(s.getBytes(defaultCharset()), 0, s.length());
        writeStream.close();
        GridFSDBFile out = gridFS.findOne(new BasicDBObject("_id", in.getId()));
        assert (out.getId().equals(in.getId()));
        assert (out.getChunkSize() == (long) GridFS.DEFAULT_CHUNKSIZE);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        out.writeTo(bout);
        String outString = new String(bout.toByteArray(), defaultCharset());
        assert (outString.equals(s));

        out.remove();
        int[] end = getCurrentCollectionCounts();
        assertEquals(start[0], end[0]);
        assertEquals(start[1], end[1]);
    }

    @Test
    public void testOutStreamSmall() throws Exception {
        testOutStream("this is a simple test");
    }

    @Test
    public void testOutStreamBig() throws Exception {
        int target = (int) (GridFS.DEFAULT_CHUNKSIZE * 3.5);
        StringBuilder buf = new StringBuilder(target);
        while (buf.length() < target) {
            buf.append("asdasdkjasldkjasldjlasjdlajsdljasldjlasjdlkasjdlaskjdlaskjdlsakjdlaskjdasldjsad");
        }
        String s = buf.toString();
        testOutStream(s);
    }

    @Test
    public void testOutStreamBigAligned() throws Exception {
        int target = (GridFS.DEFAULT_CHUNKSIZE * 4);
        StringBuilder buf = new StringBuilder(target);
        while (buf.length() < target) {
            buf.append("a");
        }
        String s = buf.toString();
        testOutStream(s);
    }

    @Test
    public void testCreateFileWithFile() throws Exception {
        URI fileURI = GridFSTest.class.getResource("/GridFSLegacy/GridFSTestFile.txt").toURI();
        GridFSInputFile in = gridFS.createFile(new File(fileURI));
        in.save();

        String expectedString = new Scanner(new File(fileURI)).useDelimiter("\\Z").next();

        GridFSDBFile out = gridFS.findOne(new BasicDBObject("_id", in.getId()));
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        out.writeTo(bout);
        String outString = new String(bout.toByteArray(), defaultCharset()).trim();

        assertEquals(expectedString, outString);
    }

    @Test
    public void testMetadata() throws Exception {

        GridFSInputFile in = gridFS.createFile("foo".getBytes(defaultCharset()));
        in.put("meta", 5);
        in.save();
        GridFSDBFile out = gridFS.findOne(new BasicDBObject("_id", in.getId()));
        assertTrue(out.get("meta").equals(5));
    }

    @Test
    public void testFind() throws Exception {

        GridFSInputFile in = gridFS.createFile(new ByteArrayInputStream("foo".getBytes(defaultCharset())), "testFind");
        in.save();
        assertNotNull(gridFS.find((ObjectId) in.getId()));
        assertNotNull(gridFS.findOne((ObjectId) in.getId()));
        assertNotNull(gridFS.findOne("testFind"));
        assertNotNull(gridFS.findOne(new BasicDBObject("_id", in.getId())));
    }

    @Test
    public void testBadChunkSize() throws Exception {
        byte[] randomBytes = new byte[256];
        GridFSInputFile inputFile = gridFS.createFile(randomBytes);
        inputFile.setFilename("bad_chunk_size.bin");
        try {
            inputFile.save(0);
            fail("should have received an exception about a chunk size being zero");
        } catch (MongoException e) {
            //We expect this exception to complain about the chunksize
            assertTrue(e.toString().contains("chunkSize must be greater than zero"));
        }
    }

    @Test
    @Category(Slow.class)
    public void testMultipleChunks() throws Exception {
        int fileSize = 1024 * 128;
        byte[] randomBytes = new byte[fileSize];
        for (int idx = 0; idx < fileSize; ++idx) {
            randomBytes[idx] = (byte) (256 * Math.random());
        }

        GridFSInputFile inputFile = gridFS.createFile(randomBytes);
        inputFile.setFilename("bad_chunk_size.bin");

        //For good measure let's save and restore the bytes
        inputFile.save(1024);
        GridFSDBFile savedFile = gridFS.findOne(new BasicDBObject("_id", inputFile.getId()));
        ByteArrayOutputStream savedFileByteStream = new ByteArrayOutputStream();
        savedFile.writeTo(savedFileByteStream);
        byte[] savedFileBytes = savedFileByteStream.toByteArray();

        assertArrayEquals(randomBytes, savedFileBytes);
    }

    @Test
    @Category(Slow.class)
    public void getBigChunkSize() throws Exception {
        GridFSInputFile file = gridFS.createFile("512kb_bucket");
        file.setChunkSize(file.getChunkSize() * 2);
        OutputStream os = file.getOutputStream();
        for (int i = 0; i < 1024; i++) {
            os.write(new byte[GridFS.DEFAULT_CHUNKSIZE / 1024 + 1]);
        }
        os.close();
    }


    @Test
    public void testInputStreamSkipping() throws Exception {
        //int chunkSize = 5;
        int chunkSize = GridFS.DEFAULT_CHUNKSIZE;
        int fileSize = (int) (7.25 * chunkSize);

        byte[] fileBytes = new byte[fileSize];
        for (int idx = 0; idx < fileSize; ++idx) {
            fileBytes[idx] = (byte) (idx % 251);
        }
        //Don't want chunks to be aligned at byte position 0

        GridFSInputFile inputFile = gridFS.createFile(fileBytes);
        inputFile.setFilename("input_stream_skipping.bin");
        inputFile.save(chunkSize);

        GridFSDBFile savedFile = gridFS.findOne(new BasicDBObject("_id", inputFile.getId()));
        InputStream inputStream = savedFile.getInputStream();

        //Quick run-through, make sure the file is as expected
        for (int idx = 0; idx < fileSize; ++idx) {
            assertEquals((byte) (idx % 251), (byte) inputStream.read());
        }

        inputStream = savedFile.getInputStream();

        long skipped = inputStream.skip(1);
        assertEquals(1, skipped);
        int position = 1;
        assertEquals((byte) (position++ % 251), (byte) inputStream.read());

        skipped = inputStream.skip(chunkSize);
        assertEquals(chunkSize, skipped);
        position += chunkSize;
        assertEquals((byte) (position++ % 251), (byte) inputStream.read());

        skipped = inputStream.skip(-1);
        assertEquals(0, skipped);
        skipped = inputStream.skip(0);
        assertEquals(0, skipped);

        skipped = inputStream.skip(3 * chunkSize);
        assertEquals(3 * chunkSize, skipped);
        position += 3 * chunkSize;
        assertEquals((byte) (position++ % 251), (byte) inputStream.read());

        //Make sure skipping works when we skip to an exact chunk boundary
        long toSkip = inputStream.available();
        skipped = inputStream.skip(toSkip);
        assertEquals(toSkip, skipped);
        position += toSkip;
        assertEquals((byte) (position++ % 251), (byte) inputStream.read());

        skipped = inputStream.skip(2 * fileSize);
        assertEquals(fileSize - position, skipped);
        assertEquals(-1, inputStream.read());
    }

    @Test
    public void testCustomFileID() throws IOException {
        // given
        int id = 1;
        gridFS.remove(new BasicDBObject("_id", id));

        int chunkSize = 10;
        int fileSize = (int) (3.25 * chunkSize);

        byte[] fileBytes = new byte[fileSize];
        for (int idx = 0; idx < fileSize; ++idx) {
            fileBytes[idx] = (byte) (idx % 251);
        }

        // when
        GridFSInputFile inputFile = gridFS.createFile(fileBytes);
        inputFile.setId(id);
        inputFile.setFilename("custom_file_id.bin");
        inputFile.save(chunkSize);
        assertEquals(id, inputFile.getId());

        // then
        GridFSDBFile savedFile = gridFS.findOne(new BasicDBObject("_id", id));
        InputStream inputStream = savedFile.getInputStream();

        for (int idx = 0; idx < fileSize; ++idx) {
            assertEquals((byte) (idx % 251), (byte) inputStream.read());
        }

        // finally
        gridFS.remove(new BasicDBObject("_id", id));
    }

    void testInOut(final String s) throws Exception {

        int[] start = getCurrentCollectionCounts();

        GridFSInputFile in = gridFS.createFile(s.getBytes(defaultCharset()));
        in.save();
        GridFSDBFile out = gridFS.findOne(new BasicDBObject("_id", in.getId()));
        assert (out.getId().equals(in.getId()));
        assert (out.getChunkSize() == (long) GridFS.DEFAULT_CHUNKSIZE);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        out.writeTo(bout);
        String outString = new String(bout.toByteArray(), defaultCharset());
        assert (outString.equals(s));

        out.remove();
        int[] end = getCurrentCollectionCounts();
        assertEquals(start[0], end[0]);
        assertEquals(start[1], end[1]);
    }

    int[] getCurrentCollectionCounts() {
        int[] i = new int[2];
        i[0] = gridFS.getFilesCollection().find().count();
        i[1] = gridFS.getChunksCollection().find().count();
        return i;
    }

    @Test(expected = IllegalArgumentException.class)
    public void testRemoveWhenObjectIdIsNull() {
        ObjectId objectId = null;
        gridFS.remove(objectId);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testRemoveWhenFileNameIsNull() {
        String fileName = null;
        gridFS.remove(fileName);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testRemoveWhenQueryIsNull() {
        DBObject dbObjectQuery = null;
        gridFS.remove(dbObjectQuery);
    }
}
