package com.netopyr.wurmloch.store;

import com.netopyr.wurmloch.crdt.Crdt;
import com.netopyr.wurmloch.crdt.CrdtCommand;
import com.netopyr.wurmloch.store.SimpleCrdt.SimpleCommand;
import io.reactivex.subscribers.TestSubscriber;
import javaslang.control.Option;
import org.testng.annotations.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

public class CrdtStoreTest {
    
    private static final String NODE_ID_1 = "N_1";
    private static final String NODE_ID_2 = "N_2";
    private static final String CRDT_ID = "ID_1";
    

    @Test
    public void shouldFindCrdts() {
        // given:
        final CrdtStore store = new CrdtStore();
        store.registerFactory(SimpleCrdt.class, SimpleCrdt::new);

        // when:
        final Option<? extends Crdt> result1 = store.findCrdt(NODE_ID_1);

        // then:
        assertThat(result1.isDefined(), is(false));

        // when:
        final SimpleCrdt expected = store.createCrdt(SimpleCrdt.class, NODE_ID_1);
        final Option<? extends Crdt> result2 = store.findCrdt(NODE_ID_1);

        // then:
        assertThat(result2.get(), is(expected));
    }

    @Test
    public void shouldAddCrdtWhileConnected() {
        // given:
        final CrdtStore store1 = new CrdtStore();
        store1.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final CrdtStore store2 = new CrdtStore();
        store2.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        store1.connect(store2);

        // when:
        final SimpleCrdt crdt1 = store1.createCrdt(SimpleCrdt.class, NODE_ID_1);

        // then:
        assertThat(store2.findCrdt(NODE_ID_1).get(), is(crdt1));

        // when:
        final SimpleCrdt crdt2 = store2.createCrdt(SimpleCrdt.class, NODE_ID_2);

        // then:
        assertThat(store1.findCrdt(NODE_ID_2).get(), is(crdt2));
    }

    @Test
    public void shouldAddCrdtAfterConnect() {
        // given:
        final CrdtStore store1 = new CrdtStore();
        store1.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final CrdtStore store2 = new CrdtStore();
        store2.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final SimpleCrdt crdt1 = store1.createCrdt(SimpleCrdt.class, NODE_ID_1);
        final SimpleCrdt crdt2 = store2.createCrdt(SimpleCrdt.class, NODE_ID_2);

        // then:
        assertThat(store2.findCrdt(NODE_ID_1).isDefined(), is(false));
        assertThat(store1.findCrdt(NODE_ID_2).isDefined(), is(false));

        // when:
        store1.connect(store2);

        // then:
        assertThat(store2.findCrdt(NODE_ID_1).get(), is(crdt1));
        assertThat(store1.findCrdt(NODE_ID_2).get(), is(crdt2));
    }

    @Test
    public void shouldNotAddExistingCrdtAfterConnect() {
        // given:
        final CrdtStore store1 = new CrdtStore();
        store1.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final CrdtStore store2 = new CrdtStore();
        store2.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        store1.connect(store2);
        final SimpleCrdt crdt1 = store1.createCrdt(SimpleCrdt.class, NODE_ID_1);
        final SimpleCrdt crdt2 = (SimpleCrdt) store2.findCrdt(NODE_ID_1).get();

        // when:
        store1.disconnect(store2);
        store1.connect(store2);

        // then:
        assertThat(store1.findCrdt(NODE_ID_1).get() == crdt1, is(true));
        assertThat(store2.findCrdt(NODE_ID_1).get() == crdt2, is(true));
    }

    @Test
    @SuppressWarnings("unchecked")
    public void shouldSendCommandsToConnectedStore() {
        // given:
        final CrdtStore store1 = new CrdtStore(NODE_ID_1);
        store1.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final TestSubscriber<CrdtDefinition> store1Subscriber = TestSubscriber.create();
        store1.subscribe(store1Subscriber);

        final CrdtStore store2 = new CrdtStore(NODE_ID_2);
        store2.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final TestSubscriber<CrdtDefinition> store2Subscriber = TestSubscriber.create();
        store2.subscribe(store2Subscriber);

        store1.connect(store2);

        final SimpleCrdt crdt1 = store1.createCrdt(SimpleCrdt.class, CRDT_ID);
        final TestSubscriber<CrdtCommand> crdt1Subscriber = TestSubscriber.create();
        crdt1.subscribe(crdt1Subscriber);

        final SimpleCrdt crdt2 = (SimpleCrdt) store2.findCrdt(CRDT_ID).get();
        final TestSubscriber<CrdtCommand> crdt2Subscriber = TestSubscriber.create();
        crdt2.subscribe(crdt2Subscriber);

        // when:
        final SimpleCommand command1_1 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        crdt1.sendCommands(command1_1);

        final SimpleCommand command2_1 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        crdt2.sendCommands(command2_1);

        final SimpleCommand command3_1 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        final SimpleCommand command3_2 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        final SimpleCommand command3_3 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        crdt1.sendCommands(command3_1, command3_2, command3_3);

        final SimpleCommand command4_1 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        final SimpleCommand command4_2 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        final SimpleCommand command4_3 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        crdt2.sendCommands(command4_1, command4_2, command4_3);

        // then:
        assertThat(store1Subscriber.valueCount(), is(1));
        store1Subscriber.assertNotComplete();
        store1Subscriber.assertNoErrors();

        crdt1Subscriber.assertValues(
                command1_1,
                command2_1,
                command3_1,
                command3_2,
                command3_3,
                command4_1,
                command4_2,
                command4_3
        );
        crdt1Subscriber.assertNotComplete();
        crdt1Subscriber.assertNoErrors();

        assertThat(store2Subscriber.valueCount(), is(1));
        store2Subscriber.assertNotComplete();
        store2Subscriber.assertNoErrors();

        crdt2Subscriber.assertValues(
                command1_1,
                command2_1,
                command3_1,
                command3_2,
                command3_3,
                command4_1,
                command4_2,
                command4_3
        );
        crdt2Subscriber.assertNotComplete();
        crdt2Subscriber.assertNoErrors();

    }

    @Test
    @SuppressWarnings("unchecked")
    public void shouldSendAllCommandsAfterConnect() {
        // given:
        final CrdtStore store1 = new CrdtStore(NODE_ID_1);
        store1.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final TestSubscriber<CrdtDefinition> store1Subscriber = TestSubscriber.create();
        store1.subscribe(store1Subscriber);

        final CrdtStore store2 = new CrdtStore(NODE_ID_2);
        store2.registerFactory(SimpleCrdt.class, SimpleCrdt::new);
        final TestSubscriber<CrdtDefinition> store2Subscriber = TestSubscriber.create();
        store2.subscribe(store2Subscriber);


        final SimpleCrdt crdt1 = store1.createCrdt(SimpleCrdt.class, CRDT_ID);
        final TestSubscriber<CrdtCommand> crdt1Subscriber = TestSubscriber.create();
        crdt1.subscribe(crdt1Subscriber);
        final SimpleCrdt crdt2 = store2.createCrdt(SimpleCrdt.class, CRDT_ID);
        final TestSubscriber<CrdtCommand> crdt2Subscriber = TestSubscriber.create();
        crdt2.subscribe(crdt2Subscriber);

        final SimpleCommand command1_1 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        crdt1.sendCommands(command1_1);

        final SimpleCommand command2_1 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        crdt2.sendCommands(command2_1);

        final SimpleCommand command3_1 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        final SimpleCommand command3_2 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        final SimpleCommand command3_3 = new SimpleCommand(NODE_ID_1, CRDT_ID);
        crdt1.sendCommands(command3_1, command3_2, command3_3);

        final SimpleCommand command4_1 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        final SimpleCommand command4_2 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        final SimpleCommand command4_3 = new SimpleCommand(NODE_ID_2, CRDT_ID);
        crdt2.sendCommands(command4_1, command4_2, command4_3);

        // when:
        store1.connect(store2);

        // then:
        assertThat(store1Subscriber.valueCount(), is(1));
        store1Subscriber.assertNotComplete();
        store1Subscriber.assertNoErrors();

        crdt1Subscriber.assertValues(
                command1_1,
                command3_1,
                command3_2,
                command3_3,
                command2_1,
                command4_1,
                command4_2,
                command4_3
        );
        crdt1Subscriber.assertNotComplete();
        crdt1Subscriber.assertNoErrors();

        assertThat(store2Subscriber.valueCount(), is(1));
        store2Subscriber.assertNotComplete();
        store2Subscriber.assertNoErrors();

        crdt2Subscriber.assertValues(
                command2_1,
                command4_1,
                command4_2,
                command4_3,
                command1_1,
                command3_1,
                command3_2,
                command3_3
        );
        crdt2Subscriber.assertNotComplete();
        crdt2Subscriber.assertNoErrors();
    }

    @Test
    public void shouldSendPartialCommandsAfterReconnect() {

    }
}