package anastore.store;

import anastore.store.Disk.BlockHandle;
import anastore.store.Disk.Slot;
import anastore.util.Pair;
import anastore.util.TimeRange;

import java.io.IOException;
import java.io.File;
import java.util.Map;

import org.junit.*;
import static org.junit.Assert.*;

public class DiskTest
{
    private static final byte[] testData =
        "Twas brillig, and the slithy toves".getBytes();
    private static final byte[] testData2 =
        "Did gyre and gimble in the wabe   ".getBytes();

    private File _dir;
    private Disk _d;

    private void create()
        throws IOException
    {
        _dir = File.createTempFile("disktest-", "");
        _dir.delete();
        _d = Disk.create(_dir);
    }

    private Map<Long, Slot> load()
        throws IOException
    {
        _d.close();
        Pair<Disk, Map<Long, Slot>> res = Disk.load(_dir);
        _d = res.first;
        return res.second;
    }

    @After
    public void cleanup()
        throws IOException
    {
        if (_dir.exists()) {
            for (File f : _dir.listFiles())
                f.delete();
            _dir.delete();
        }
    }

    @Test(expected=IllegalArgumentException.class)
    public void createExistingFails()
        throws IOException
    {
        _dir = File.createTempFile("disktest-", "");
        _dir.deleteOnExit();
        _dir.delete();
        _dir.mkdir();
        Disk.create(_dir);
    }

    @Test(expected=IllegalArgumentException.class)
    public void loadNonexistingFails()
        throws IOException
    {
        _dir = File.createTempFile("disktest-", "");
        _dir.deleteOnExit();
        _dir.delete();
        Disk.load(_dir);
    }

    @Test
    public void createThenLoadIsEmpty()
        throws IOException
    {
        create();
        assertTrue(load().isEmpty());
    }

    @Test
    public void slotAllocateIsLazy()
        throws IOException
    {
        create();
        _d.newSlot(42);
        assertTrue(load().isEmpty());
    }

    @Test
    public void rereadBlock()
        throws IOException
    {
        create();
        Slot slot = _d.newSlot(42);
        BlockHandle bh = slot.add(new TimeRange(43), testData);
        bh.testForgetData();
        assertArrayEquals(testData, bh.read());
    }

    @Test
    public void reloadBlock()
        throws IOException
    {
        reloadHelper(1, 0);
    }

    @Test
    public void reloadMultipleBlocks()
        throws IOException
    {
        reloadHelper(5, 0);
    }

    @Test
    public void reloadTwice()
        throws IOException
    {
        reloadHelper(5, 5);
    }

    private void reloadHelper(int count, int count2)
        throws IOException
    {
        create();
        TimeRange trs[] = new TimeRange[count];
        for (int i = 0; i < count; ++i) {
            Slot slot = _d.newSlot(42+i);
            trs[i] = new TimeRange(142+i, 242+i);
            slot.add(trs[i], testData);
        }

        Map<Long, Slot> slots = load();
        assertEquals(count, slots.size());
        for (int i = 0; i < count; ++i) {
            checkSlotEntry(slots, 42+i, trs[i], testData);
        }

        if (count2 == 0)
            return;
        TimeRange trs2[] = new TimeRange[count2];
        for (int i = 0; i < count2; ++i) {
            Slot slot = _d.newSlot(142+i);
            trs2[i] = new TimeRange(242+i, 342+i);
            slot.add(trs2[i], testData2);
        }

        slots = load();
        assertEquals(count+count2, slots.size());
        for (int i = 0; i < count; ++i) {
            checkSlotEntry(slots, 42+i, trs[i], testData);
        }
        for (int i = 0; i < count2; ++i) {
            checkSlotEntry(slots, 142+i, trs2[i], testData2);
        }
    }

    private void checkSlotEntry(Map<Long, Slot> slots,
                                long id, TimeRange tr, byte[] data)
        throws IOException
    {
        Slot slot = slots.get(id);
        assertNotNull(slot);
        assertEquals(id, slot.getID());
        BlockHandle bh = slot.getLatest();
        assertEquals(id, bh.getID());
        assertEquals(tr.getLowerBound(), bh.getRange().getLowerBound());
        assertEquals(tr.hasUpperBound(), bh.getRange().hasUpperBound());
        if (tr.hasUpperBound())
            assertEquals(tr.getUpperBound(), bh.getRange().getUpperBound());
        assertArrayEquals(data, bh.read());
    }

    @Test
    public void addNewerBlock()
        throws IOException
    {
        addXBlockHelper(true);
    }

    @Test
    public void addOlderBlock()
        throws IOException
    {
        addXBlockHelper(false);
    }

    private void addXBlockHelper(boolean newer)
        throws IOException
    {
        create();
        Slot slot = _d.newSlot(42);
        TimeRange tr = new TimeRange(43, 44);
        BlockHandle bh = slot.add(tr, testData);
        TimeRange tr2 = new TimeRange(newer ? 45 : 41, newer ? 46 : 42);
        BlockHandle bh2 = slot.add(tr2, testData2);

        assertArrayEquals(testData, bh.read());
        assertArrayEquals(testData2, bh2.read());

        bh.testForgetData();
        bh2.testForgetData();
        assertArrayEquals(testData, bh.read());
        assertArrayEquals(testData2, bh2.read());

        Map<Long, Slot> slots = load();
        assertEquals(1, slots.size());
        if (newer)
            checkSlotEntry(slots, 42, tr2, testData2);
        else
            checkSlotEntry(slots, 42, tr, testData);
    }

    @Test(expected=IllegalArgumentException.class)
    public void addSameBlockFails()
        throws IOException
    {
        Slot slot;
        try {
            create();
            slot = _d.newSlot(42);
            slot.add(new TimeRange(43, 44), testData);
        } catch (IllegalArgumentException e) {
            fail();
            return;
        }
        slot.add(new TimeRange(43, 44), testData2);
    }

    @Test(expected=IllegalArgumentException.class)
    public void addDifferentSizeBlockFails()
        throws IOException
    {
        Slot slot;
        try {
            create();
            slot = _d.newSlot(42);
            slot.add(new TimeRange(43, 44), testData);
        } catch (IllegalArgumentException e) {
            fail();
            return;
        }
        slot.add(new TimeRange(45, 46), new byte[testData.length-1]);
    }
}
