package com.github.zhongl.ipage;

import com.github.zhongl.util.FileBase;
import com.google.common.io.Files;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Ignore;
import org.junit.Test;

import static com.github.zhongl.ipage.Recovery.IndexRecoverer;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

/** @author <a href="mailto:zhong.lunfu@gmail.com">zhongl<a> */
public class BucketsTest extends FileBase {

    private Buckets buckets;

    @Override
    public void tearDown() throws Exception {
        if (buckets != null) buckets.close();
        super.tearDown();
    }

    @Test
    public void useDefaultSizeIfInvalidBucketSizeWereSet() throws Exception {
        file = testFile("invalidBucketSize");
        assertThat(new Buckets(file, -1).size(), is(256));
    }

    @Test
    public void putAndGet() throws Exception {
        file = testFile("putAndGet");
        buckets = new Buckets(file, 1);

        Md5Key key = Md5Key.valueOf("key".getBytes());

        long offset = 7L;
        assertThat(buckets.put(key, offset), is(nullValue()));
        assertThat(buckets.get(key), is(offset));

        buckets.close();

        buckets = new Buckets(file, 1);
        assertThat(buckets.get(key), is(offset)); // assert sync to file
    }

    @Test
    public void remove() throws Exception {
        file = testFile("remove");
        buckets = new Buckets(file, 1);

        Md5Key key = Md5Key.valueOf("key".getBytes());
        Long offset = 7L;
        assertThat(buckets.put(key, offset), is(nullValue()));
        assertThat(buckets.remove(key), is(offset));
        assertThat(buckets.get(key), is(nullValue()));
    }

    @Test
    public void validateWithoutRecovery() throws Exception {
        file = testFile("validateAndRecoverBy");

        buckets = new Buckets(file, 1);
        IndexRecoverer indexRecoverer = mock(IndexRecoverer.class);
        assertThat(buckets.validateAndRecoverBy(indexRecoverer), is(false));// validateAndRecoverBy a empty bucket

        buckets.put(Md5Key.valueOf("key".getBytes()), 7L);
        buckets.close();
        buckets = new Buckets(file, 1);
        doReturn(new Record("key".getBytes())).when(indexRecoverer).getRecordIn(anyLong());
        assertThat(buckets.validateAndRecoverBy(indexRecoverer), is(false)); // validateAndRecoverBy a exist bucket
    }

    @Test
    public void validateWithRecovery() throws Exception {
        file = testFile("validateWithRecovery");
        byte[] md5Bytes = DigestUtils.md5("value");
        byte[] bucketMd5 = DigestUtils.md5("bucket");
        byte[] brokenBucketContent = Bytes.concat(
                new byte[] {1},
                md5Bytes,
                Longs.toByteArray(7L),
                new byte[4055],
                bucketMd5
        );
        Files.write(brokenBucketContent, file); // mock broken buckets file.

        buckets = new Buckets(file, 1);

        IndexRecoverer indexRecoverer = mock(IndexRecoverer.class);
        doReturn(new Record("broken".getBytes())).when(indexRecoverer).getRecordIn(anyLong());
        assertThat(buckets.validateAndRecoverBy(indexRecoverer), is(true));
    }

    @Test
    public void putInReleasedSlot() throws Exception {
        file = testFile("putInReleasedSlot");
        buckets = new Buckets(file, 1);

        fillFullBuckets();

        Md5Key key0 = Md5Key.valueOf(Ints.toByteArray(0));
        Md5Key key1 = Md5Key.valueOf(Ints.toByteArray(1));
        buckets.remove(key0);
        buckets.remove(key1);
        assertThat(buckets.put(key1, 7L), is(nullValue()));
        assertThat(buckets.put(key0, 7L), is(nullValue())); // no exception means new item index put in released slot.
    }

    @Test
    public void getAndRemoveEmplyBucket() throws Exception {
        file = testFile("getAndRemoveEmplyBucket");
        buckets = new Buckets(file, 1);

        Md5Key key = Md5Key.valueOf(Ints.toByteArray(1));
        assertThat(buckets.get(key), is(nullValue()));
        assertThat(buckets.remove(key), is(nullValue()));
    }

    @Test
    public void getAndRemoveByInvalidKey() throws Exception {
        file = testFile("getAndRemoveByInvalidKey");
        buckets = new Buckets(file, 1);

        fillFullBuckets();

        Md5Key invalidKey = Md5Key.valueOf(Ints.toByteArray(163));

        assertThat(buckets.get(invalidKey), is(nullValue()));
        assertThat(buckets.remove(invalidKey), is(nullValue()));
    }

    @Test(expected = IllegalStateException.class)
    public void unknownSlotState() throws Exception {
        file = testFile("unknownSlotState");
        buckets = new Buckets(file, 1);
        Files.write(new byte[] {3}, file);  // write a unknown slot state
        buckets.get(Md5Key.valueOf(Ints.toByteArray(1))); // trigger exception
    }

    @Test(expected = OverflowException.class)
    public void noSlotForNewItemIndex() throws Exception {
        file = testFile("noSlotForNewItemIndex");
        fillFullBuckets();
        Md5Key key163 = Md5Key.valueOf(Ints.toByteArray(163));
        buckets.put(key163, 7L);  // trigger exception
    }

    @Test
    public void replace() throws Exception {
        file = testFile("replace");
        buckets = new Buckets(file, 1);
        Md5Key key = Md5Key.valueOf("key".getBytes());
        assertThat(buckets.put(key, 7L), is(nullValue()));
        assertThat(buckets.put(key, 4L), is(7L));
    }

    @Test
    @Ignore("TODO")
    public void cleanup() throws Exception {
        // TODO cleanup
    }

    private void fillFullBuckets() throws Exception {
        buckets = new Buckets(file, 1);
        for (int i = 0; i < 163; i++) {
            buckets.put(Md5Key.valueOf(Ints.toByteArray(i)), 10L);
        }
    }

}

@@ -2822,16 +2822,17 @@
 md5Bytes
+0
  = Diges
@@ -2848,16 +2848,71 @@
 5(%22value
+0%22);%0A        byte%5B%5D md5Bytes1 = DigestUtils.md5(%22value1
 %22);%0A    
@@ -3068,16 +3068,115 @@
 md5Bytes
+0,%0A                Longs.toByteArray(4L),%0A                new byte%5B%5D %7B1%7D,%0A                md5Bytes1
 ,%0A      
@@ -3456,32 +3456,120 @@
 coverer.class);%0A
+        doReturn(new Record(%22value0%22.getBytes())).when(indexRecoverer).getRecordIn(4L);%0A
         doReturn
@@ -3631,35 +3631,44 @@
 getRecordIn(
-anyLong());
+7L); // broken index
 %0A        ass

Improve test coverage.