package freenet.support.io;

import static org.junit.Assert.*;

import java.io.IOException;
import java.util.Random;

import org.junit.Test;

import freenet.support.api.RandomAccessBuffer;

/** Base class for testing RandomAccessBuffer's. */
public abstract class RandomAccessBufferTestBase {
    
    /** Size list for small tests i.e. stuff that definitely fits in RAM */
    protected final int[] sizeList;
    /** Size list for big tests i.e. stuff that might not fit in RAM */
    private final long[] fullSizeList;
    
    protected RandomAccessBufferTestBase(int[] allSmallTests) {
        sizeList = allSmallTests;
        fullSizeList = new long[sizeList.length];
        for(int i=0;i<sizeList.length;i++) fullSizeList[i] = sizeList[i];
    }
    
    protected RandomAccessBufferTestBase(int[] smallTests, long[] bigTests) {
        sizeList = smallTests;
        fullSizeList = bigTests;
    }
    
    /** Construct an instance of a given size. 
     * @throws IOException */
    protected abstract RandomAccessBuffer construct(long size) throws IOException;
    
    private void innerTestSize(long sz) throws IOException {
        RandomAccessBuffer raf = construct(sz);
        assertEquals(raf.size(), sz);
        raf.close();
        raf.free();
    }

    /** Test that we can create and free a RandomAccessBuffer of various sizes, and it returns the correct
     * size. */
    @Test
    public void testSize() throws IOException {
        for(long size : fullSizeList)
            innerTestSize(size);
    }
    
    private static final int BUFFER_SIZE = 65536;
    
    @Test
    public void testFormula() throws IOException {
        Random r = new Random(2126);
        Formula modulo256 = new Formula() {

            @Override
            public byte getByte(long offset) {
                return (byte)offset;
            }
            
        };
        Formula modulo57 = new Formula() {

            @Override
            public byte getByte(long offset) {
                return (byte)(offset % 57);
            }
            
        };
        for(long size : fullSizeList) {
            innerTestFormula(size, r, modulo256);
            innerTestFormula(size, r, modulo57);
        }
    }
        
    protected interface Formula {
        byte getByte(long offset);
    }
    
    /** Write using a given formula in random small writes, then check using random small reads. */
    protected void innerTestFormula(long sz, Random r, Formula f) throws IOException {
        RandomAccessBuffer raf = construct(sz);
        assertEquals(raf.size(), sz);
        int x = 0;
        // Write (and check as go)
        while(x < sz) {
            int maxRead = (int)Math.min(BUFFER_SIZE, sz - x);
            int toRead = maxRead == 1 ? 1 : r.nextInt(maxRead-1)+1;
            byte[] buf = new byte[toRead];
            for(int i=0;i<buf.length;i++) buf[i] = f.getByte(i+x);
            raf.pwrite(x, buf, 0, toRead);
            for(int i=0;i<buf.length;i++) buf[i] = (byte)~buf[i];
            raf.pread(x, buf, 0, toRead);
            for(int i=0;i<buf.length;i++) assertEquals(buf[i], f.getByte(i+x));
            x += toRead;
        }
        // Read
        while(x < sz) {
            int maxRead = (int)Math.min(BUFFER_SIZE, sz - x);
            int toRead = r.nextInt(maxRead-1)+1;
            byte[] buf = new byte[toRead];
            raf.pread(x, buf, 0, toRead);
            for(int i=0;i<buf.length;i++) assertEquals(buf[i], f.getByte(i+x));
            x += toRead;
        }
        x = 0;
        raf.close();
        raf.free();
    }
    
    /** Test that we can't write or read after the size limit */
    @Test
    public void testWriteOverLimit() throws IOException {
        Random r = new Random(21092506);
        innerTestWriteOverLimit(0L, 1);
        innerTestWriteOverLimit(1, 1);
        innerTestWriteOverLimit(1, 1024);
        for(int i=0;i<10;i++) {
            innerTestWriteOverLimit(1024*1024+1, r.nextInt(1024));
        }
        innerTestWriteOverLimit(1024*1024+1, 1024*1024);
        innerTestWriteOverLimit(1024*1024+1, 1024*1024+2);
        for(long size : fullSizeList) {
            innerTestWriteOverLimit(size, 1024);
        }
        for(int size : sizeList) {
            innerTestWriteOverLimit(size, size);
            innerTestWriteOverLimit(size, size+1);
        }
    }
    
    private void innerTestWriteOverLimit(long sz, int choppedBytes) throws IOException {
        RandomAccessBuffer raf = construct(sz);
        assertEquals(raf.size(), sz);
        long startAt = sz - choppedBytes;
        byte[] buf = new byte[choppedBytes];
        if(sz != 0 && choppedBytes < sz) {
            if(startAt >= 0)
                readWriteMustSucceed(raf, startAt, buf, 0, buf.length); // Read, write up to the end work.
            else
                try {
                    readWriteMustSucceed(raf, startAt, buf, 0, buf.length); // Read, write up to the end work.
                    fail("Should fail to read at negative index");
                } catch (IllegalArgumentException e) {
                    // Ok.
                }
        }
        if(startAt+1 >= 0)
            readWriteMustFail(raf, startAt+1, buf, 0, buf.length); // Read, write over the end fail.
        else
            try {
                readWriteMustSucceed(raf, startAt+1, buf, 0, buf.length); // Read, write up to the end work.
                fail("Should fail to read at negative index");
            } catch (IllegalArgumentException e) {
                // Ok.
            }
        readWriteMustFail(raf, sz, buf, 0, buf.length); // Read, write at the end fail.
        readWriteMustFail(raf, sz+1, buf, 0, buf.length); // One byte into end
        readWriteMustFail(raf, sz+1025, buf, 0, buf.length); // 1KB in
        readWriteMustFail(raf, sz+buf.length, buf, 0, buf.length);
        raf.close();
        raf.free();
    }

    private void readWriteMustSucceed(RandomAccessBuffer raf, long startAt, byte[] buf, int offset, int length) throws IOException {
        raf.pread(startAt, buf, 0, buf.length); // Should work
        raf.pwrite(startAt, buf, 0, buf.length); // Should work
    }

    private void readWriteMustFail(RandomAccessBuffer raf, long startAt, byte[] buf, int offset, int length) throws IOException {
        if(length == 0) return; // NOP.
        try {
            raf.pread(startAt, buf, 0, buf.length); // Should work
            fail("Must throw!");
        } catch (IOException e) {
            // Ok.
        }
        try {
            raf.pwrite(startAt, buf, 0, buf.length); // Should work
            fail("Must throw!");
        } catch (IOException e) {
            // Ok.
        }
    }

    @Test
    public void testClose() throws IOException {
        // Try to cover any thresholds for e.g. moving to disk.
        // Implementations should add their own tests according to known thresholds (white box).
        for(long size : fullSizeList)
            innerTestClose(size);
    }
    
    /** Test that after closing a RandomAccessBuffer we cannot read from it or write to it */
    protected void innerTestClose(long sz) throws IOException {
        RandomAccessBuffer raf = construct(sz);
        raf.close();
        byte[] buf = new byte[(int)Math.min(1024, sz)];
        readWriteMustFail(raf, 0L, buf, 0, buf.length);
        raf.free();
    }
    
    @Test
    public void testArray() throws IOException {
        Random r = new Random(21162506);
        for(int size : sizeList)
            innerTestArray(size, r, false);
    }
    
    /** Create an array, fill it with random numbers, write it sequentially to the 
     * RandomAccessBuffer, then read randomly and compare. */
    protected void innerTestArray(int len, Random r, boolean readOnly) throws IOException {
        if(len == 0) return;
        byte[] buf = new byte[len];
        r.nextBytes(buf);
        RandomAccessBuffer raf = construct(len);
        raf.pwrite(0L, buf, 0, buf.length);
        for(int i=0;i<100;i++) {
            int end = len == 1 ? 1 : r.nextInt(len)+1;
            int start = r.nextInt(end);
            checkArraySectionEqualsReadData(buf, raf, start, end, readOnly);
        }
        checkArraySectionEqualsReadData(buf, raf, 0, len, readOnly);
        if(len > 1)
            checkArraySectionEqualsReadData(buf, raf, 1, len-1, readOnly);
        raf.close();
        raf.free();
    }

    /** Check that the array section equals the read data, then write it and repeat the check. */
    public static void checkArraySectionEqualsReadData(byte[] buf, RandomAccessBuffer raf, int start, int end, boolean readOnly) throws IOException {
        int len = end - start;
        if(len == 0) return;
        byte[] tmp = new byte[len];
        raf.pread(start, tmp, 0, len);
        for(int i=0;i<len;i++)
            assertEquals(tmp[i], buf[start+i]);
        if(!readOnly) {
            raf.pwrite(start, buf, start, len);
        }
        for(int i=0;i<len;i++) tmp[i] = 0;
        raf.pread(start, tmp, 0, len);
        for(int i=0;i<len;i++)
            assertEquals(tmp[i], buf[start+i]);
    }

}
