package freenet.crypt;

import static org.junit.Assert.*;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;

import org.junit.Test;

import freenet.support.api.Bucket;
import freenet.support.io.ArrayBucket;
import freenet.support.io.BucketTools;
import freenet.support.io.FileUtil;
import freenet.support.io.NoCloseProxyOutputStream;

public class AEADStreamsTest {
    
    @Test
    public void testSuccessfulRoundTrip() throws IOException {
        Random random = new Random(0x96231307);
        for(int i=0;i<10;i++) {
            ArrayBucket input = new ArrayBucket();
            BucketTools.fill(input, random, 65536);
            checkSuccessfulRoundTrip(16, random, input, new ArrayBucket(), new ArrayBucket());
            checkSuccessfulRoundTrip(24, random, input, new ArrayBucket(), new ArrayBucket());
            checkSuccessfulRoundTrip(32, random, input, new ArrayBucket(), new ArrayBucket());
        }
    }
    
    @Test
    public void testCorruptedRoundTrip() throws IOException {
        Random random = new Random(0x96231307); // Same seed as first test, intentionally.
        for(int i=0;i<10;i++) {
            ArrayBucket input = new ArrayBucket();
            BucketTools.fill(input, random, 65536);
            checkFailedCorruptedRoundTrip(16, random, input, new ArrayBucket(), new ArrayBucket());
            checkFailedCorruptedRoundTrip(24, random, input, new ArrayBucket(), new ArrayBucket());
            checkFailedCorruptedRoundTrip(32, random, input, new ArrayBucket(), new ArrayBucket());
        }
    }
    
    @Test
    public void testTruncatedReadsWritesRoundTrip() throws IOException {
        Random random = new Random(0x49ee92f5);
        ArrayBucket input = new ArrayBucket();
        BucketTools.fill(input, random, 512*1024);
        checkSuccessfulRoundTripRandomSplits(16, random, input, new ArrayBucket(), new ArrayBucket());
        checkSuccessfulRoundTripRandomSplits(24, random, input, new ArrayBucket(), new ArrayBucket());
        checkSuccessfulRoundTripRandomSplits(32, random, input, new ArrayBucket(), new ArrayBucket());
    }
    
    public void checkSuccessfulRoundTrip(int keysize, Random random, Bucket input, Bucket output, Bucket decoded) throws IOException {
        byte[] key = new byte[keysize];
        random.nextBytes(key);
        OutputStream os = output.getOutputStream();
        AEADOutputStream cos = AEADOutputStream.innerCreateAES(os, key, random);
        BucketTools.copyTo(input, cos, -1);
        cos.close();
        assertTrue(output.size() > input.size());
        InputStream is = output.getInputStream();
        AEADInputStream cis = AEADInputStream.createAES(is, key);
        BucketTools.copyFrom(decoded, cis, -1);
        assertEquals(decoded.size(), input.size());
        assertTrue(BucketTools.equalBuckets(decoded, input));
    }

    public void checkFailedCorruptedRoundTrip(int keysize, Random random, Bucket input, Bucket output, Bucket decoded) throws IOException {
        byte[] key = new byte[keysize];
        random.nextBytes(key);
        OutputStream os = output.getOutputStream();
        CorruptingOutputStream kos = new CorruptingOutputStream(os, 16L, input.size() + 16, 10, random);
        AEADOutputStream cos = AEADOutputStream.innerCreateAES(kos, key, random);
        BucketTools.copyTo(input, cos, -1);
        cos.close();
        assertTrue(output.size() > input.size());
        InputStream is = output.getInputStream();
        AEADInputStream cis = AEADInputStream.createAES(is, key);
        try {
            BucketTools.copyFrom(decoded, cis, -1);
            cis.close();
            fail("Checksum error should have been seen");
        } catch (AEADVerificationFailedException e) {
            // Expected.
        }
        assertEquals(decoded.size(), input.size());
        assertFalse(BucketTools.equalBuckets(decoded, input));
    }

    public void checkSuccessfulRoundTripRandomSplits(int keysize, Random random, Bucket input, Bucket output, Bucket decoded) throws IOException {
        byte[] key = new byte[keysize];
        random.nextBytes(key);
        OutputStream os = output.getOutputStream();
        AEADOutputStream cos = AEADOutputStream.innerCreateAES(os, key, random);
        BucketTools.copyTo(input, new RandomShortWriteOutputStream(cos, random), -1);
        cos.close();
        assertTrue(output.size() > input.size());
        InputStream is = output.getInputStream();
        AEADInputStream cis = AEADInputStream.createAES(is, key);
        BucketTools.copyFrom(decoded, new RandomShortReadInputStream(cis, random), -1);
        assertEquals(decoded.size(), input.size());
        assertTrue(BucketTools.equalBuckets(decoded, input));
    }
    
    /** Check whether we can close the stream early. 
     * @throws IOException */
    @Test
    public void testCloseEarly() throws IOException {
        ArrayBucket input = new ArrayBucket();
        BucketTools.fill(input, 2048);
        int keysize = 16;
        Random random = new Random(0x47f6709f);
        byte[] key = new byte[keysize];
        random.nextBytes(key);
        Bucket output = new ArrayBucket();
        OutputStream os = output.getOutputStream();
        AEADOutputStream cos = AEADOutputStream.innerCreateAES(os, key, random);
        BucketTools.copyTo(input, cos, 2048);
        cos.close();
        InputStream is = output.getInputStream();
        AEADInputStream cis = AEADInputStream.createAES(is, key);
        byte[] first1KReadEncrypted = new byte[1024];
        new DataInputStream(cis).readFully(first1KReadEncrypted);
        byte[] first1KReadOriginal = new byte[1024];
        new DataInputStream(input.getInputStream()).readFully(first1KReadOriginal);
        assertTrue(Arrays.equals(first1KReadEncrypted, first1KReadOriginal));
        cis.close();
    }
    
    /** If we close the stream early but there is garbage after that point, it should throw on
     * close(). 
     * @throws IOException */
    @Test
    public void testGarbageAfterClose() throws IOException {
        ArrayBucket input = new ArrayBucket();
        BucketTools.fill(input, 1024);
        int keysize = 16;
        Random random = new Random(0x47f6709f);
        byte[] key = new byte[keysize];
        random.nextBytes(key);
        Bucket output = new ArrayBucket();
        OutputStream os = output.getOutputStream();
        AEADOutputStream cos = AEADOutputStream.innerCreateAES(new NoCloseProxyOutputStream(os), key, random);
        BucketTools.copyTo(input, cos, -1);
        cos.close();
        // Now write garbage.
        FileUtil.fill(os, 1024);
        os.close();
        InputStream is = output.getInputStream();
        AEADInputStream cis = AEADInputStream.createAES(is, key);
        byte[] first1KReadEncrypted = new byte[1024];
        new DataInputStream(cis).readFully(first1KReadEncrypted);
        byte[] first1KReadOriginal = new byte[1024];
        new DataInputStream(input.getInputStream()).readFully(first1KReadOriginal);
        assertTrue(Arrays.equals(first1KReadEncrypted, first1KReadOriginal));
        try {
            cis.close();
            fail("Hash should be bogus due to garbage data at end");
        } catch (AEADVerificationFailedException e) {
            // Expected.
        }
    }

}
