package lsr.paxos.storage;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import lsr.common.DirectoryHelper;
import lsr.common.Reply;
import lsr.common.RequestId;
import lsr.paxos.Snapshot;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class FullSSDiscWriterTest {
    private String directoryPath = "bin/logs";
    private FullSSDiscWriter writer;

    @Before
    public void setUp() throws FileNotFoundException {
        DirectoryHelper.create(directoryPath);
        writer = new FullSSDiscWriter(directoryPath);
    }

    @After
    public void tearDown() throws IOException {
        writer.close();
        DirectoryHelper.delete(directoryPath);
    }

    @Test
    public void shouldWriteOnInstanceViewChange() throws IOException {
        writer.changeInstanceView(1, 2);

        ByteBuffer buffer = ByteBuffer.allocate(9);
        buffer.put((byte) 1); // type
        buffer.putInt(1); // id
        buffer.putInt(2); // view

        assertArrayEquals(buffer.array(), readFile(directoryPath + "/sync.0.log"));
    }

    @Test
    public void shouldWriteOnInstanceValueChange() throws IOException {
        byte[] value = new byte[] {1, 2};
        writer.changeInstanceValue(1, 2, value);

        ByteBuffer buffer = ByteBuffer.allocate(15);
        buffer.put((byte) 2); // type
        buffer.putInt(1); // id
        buffer.putInt(2); // view
        buffer.putInt(2); // value size
        buffer.put(value); // value

        String path = directoryPath + "/sync.0.log";
        assertArrayEquals(readFile(path), buffer.array());
    }

    @Test
    public void shouldGetNextLogNumber() throws IOException {
        String[] s = new String[] {"sync.0.log", "invalid", "sync.2.log", "sync.1.log"};

        int lastLogNumber = writer.getLastLogNumber(s);
        assertEquals(lastLogNumber, 2);
    }

    @Test
    public void shouldChangeViewNumber() throws IOException {
        writer.changeViewNumber(5);

        DataInputStream stream = new DataInputStream(new FileInputStream(directoryPath +
                                                                         "/sync.0.view"));
        int view = stream.readInt();
        stream.close();
        assertEquals(view, 5);
    }

    @Test
    public void shouldLoad() throws IOException {
        byte[] value = new byte[] {1, 2};
        byte[] newValue = new byte[] {1, 2, 3};
        writer.changeInstanceValue(1, 2, value);
        writer.changeInstanceValue(2, 2, value);
        writer.changeInstanceValue(1, 3, newValue);
        writer.changeInstanceView(1, 4);
        writer.close();

        writer = new FullSSDiscWriter(directoryPath);
        ConsensusInstance[] instances = writer.load().toArray(new ConsensusInstance[0]);
        writer.close();

        ConsensusInstance instance1 = instances[0];
        ConsensusInstance instance2 = instances[1];

        assertEquals(4, instance1.getView());
        assertArrayEquals(newValue, instance1.getValue());

        assertEquals(2, instance2.getView());
        assertArrayEquals(value, instance2.getValue());
    }

    @Test
    public void shouldLoadCorruptedData() throws IOException {
        byte[] value = new byte[] {1, 2};
        byte[] newValue = new byte[] {1, 2, 3};
        writer.changeInstanceValue(1, 2, value);
        writer.changeInstanceValue(2, 2, value);
        writer.changeInstanceValue(1, 3, newValue);
        writer.changeInstanceView(1, 4);
        writer.close();

        FileOutputStream stream = new FileOutputStream(directoryPath + "/sync.0.log", true);
        stream.write(new byte[] {1, 2, 3});
        stream.close();

        writer = new FullSSDiscWriter(directoryPath);
        ConsensusInstance[] instances = writer.load().toArray(new ConsensusInstance[0]);
        writer.close();

        ConsensusInstance instance1 = instances[0];
        ConsensusInstance instance2 = instances[1];

        assertEquals(4, instance1.getView());
        assertArrayEquals(newValue, instance1.getValue());

        assertEquals(2, instance2.getView());
        assertArrayEquals(value, instance2.getValue());
    }

    @Test
    public void shouldLoadDataFromMoreFiles() throws IOException {
        byte[] value = new byte[] {1, 2};
        byte[] newValue = new byte[] {1, 2, 3};
        writer.changeInstanceValue(1, 2, value);
        writer.changeInstanceValue(2, 2, value);
        writer.changeInstanceValue(1, 3, newValue);
        writer.close();

        writer = new FullSSDiscWriter(directoryPath);
        writer.changeInstanceView(1, 4);
        writer.close();

        writer = new FullSSDiscWriter(directoryPath);
        ConsensusInstance[] instances = writer.load().toArray(new ConsensusInstance[0]);
        writer.close();

        ConsensusInstance instance1 = instances[0];
        ConsensusInstance instance2 = instances[1];

        assertEquals(4, instance1.getView());
        assertArrayEquals(newValue, instance1.getValue());

        assertEquals(2, instance2.getView());
        assertArrayEquals(value, instance2.getValue());
    }

    @Test
    public void shouldLoadViewNumber() throws IOException {
        writer.changeViewNumber(5);
        writer.close();

        writer = new FullSSDiscWriter(directoryPath);
        int view = writer.loadViewNumber();
        writer.close();

        assertEquals(view, 5);
    }

    @Test
    public void shouldWriteNewSnapshot() throws IOException {
        Snapshot snapshot = new Snapshot();

        snapshot.setNextInstanceId(2);
        snapshot.setValue(new byte[] {1, 2, 3});

        Map<Long, Reply> lastReplyForClient = new HashMap<Long, Reply>();
        lastReplyForClient.put((long) 1, new Reply(new RequestId(3, 1), new byte[] {1}));
        snapshot.setLastReplyForClient(lastReplyForClient);

        List<Reply> partialResponseCache = Arrays.asList(
                new Reply(new RequestId(1, 1), new byte[] {1, 2, 3}),
                new Reply(new RequestId(2, 2), new byte[] {1, 2, 3, 4}));
        snapshot.setPartialResponseCache(partialResponseCache);

        writer.newSnapshot(snapshot);
        writer.close();

        writer = new FullSSDiscWriter(directoryPath);
        writer.load();
        Snapshot actual = writer.getSnapshot();

        assertArrayEquals(snapshot.getValue(), actual.getValue());
        assertEquals(snapshot.getNextInstanceId(), actual.getNextInstanceId());
        assertEquals(snapshot.getNextRequestSeqNo(), actual.getNextRequestSeqNo());
        assertEquals(snapshot.getStartingRequestSeqNo(), actual.getStartingRequestSeqNo());
    }

    private byte[] readFile(String path) throws IOException {
        FileInputStream stream = new FileInputStream(path);
        int length = stream.available(); // danger
        byte[] value = new byte[length];
        stream.read(value);
        stream.close();
        return value;
    }
}
