package de.accso.flexinale.common.application.caching;

import de.accso.flexinale.common.application.caching.InMemoryCache;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import org.junit.jupiter.api.Test;

import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

class InMemoryCacheTest {

    private record Data(String text, int number) { }

    @Test
    void testEmptyCacheContainsNothing() {
        // arrange
        Random random = new Random();

        // act
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();

        // assert
        assertThat(sut.size()).isEqualTo(0);

        Data data = new Data("" + random.nextInt(), random.nextInt());
        assertThat(sut.contains(data)).isFalse();

        assertThat(sut.keys().isEmpty()).isTrue();
        assertThat(sut.values().isEmpty()).isTrue();
        assertThat(sut.entrySet().isEmpty()).isTrue();
    }

    @Test
    void testGet() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);
        sut.put(id, data);

        // act
        Data retrievedData = sut.get(id);

        // assert
        assertThat(retrievedData).isEqualTo(data);
    }

    @Test
    void testGetForNotPuttedId() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);
        sut.put(id, data);

        // act
        Identifiable.Id newId = Identifiable.Id.of();
        Data retrievedData = sut.get(newId);

        // assert
        assertThat(retrievedData).isNull();
    }

    @Test
    void testPutOnce() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);

        // act
        Data oldData = sut.put(id, data);

        // assert
        assertThat(oldData).isNull();
    }

    @Test
    void testPutTwice() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data1 = new Data("text1", 42);
        Data data2 = new Data("text2", 21);

        // act
        Data oldData0 = sut.put(id, data1);
        Data oldData1 = sut.put(id, data2);

        // assert
        assertThat(oldData0).isNull();
        assertThat(oldData1).isEqualTo(data1);
    }

    @Test
    void testRemoveExistingData() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);

        // act
        Data oldData0 = sut.put(id, data);
        Data removedData = sut.remove(id);
        Data notFoundData = sut.get(id);

        // assert
        assertThat(oldData0).isNull();
        assertThat(removedData).isEqualTo(data);
        assertThat(notFoundData).isNull();
    }

    @Test
    void testRemoveNotExistingDataDoesNothing() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();

        // act
        Data removedData = sut.remove(id);
        Data notFoundData = sut.get(id);

        // assert
        assertThat(removedData).isNull();
        assertThat(notFoundData).isNull();
    }

    @Test
    void testKeys() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id1 = Identifiable.Id.of();
        Data data1 = new Data("text", 42);
        Identifiable.Id id2 = Identifiable.Id.of();
        Data data2 = new Data("abc", 21);

        sut.put(id1, data1);
        sut.put(id2, data2);

        // act
        Set<Identifiable.Id> retrievedKeys = sut.keys();

        // assert
        assertThat(retrievedKeys).isEqualTo(Set.of(id1, id2));
    }

    @Test
    void testValues() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id1 = Identifiable.Id.of();
        Data data1 = new Data("text", 42);
        Identifiable.Id id2 = Identifiable.Id.of();
        Data data2 = new Data("abc", 21);

        sut.put(id1, data1);
        sut.put(id2, data2);

        // act
        Set<Data> retrievedKeys = sut.values();

        // assert
        assertThat(retrievedKeys).isEqualTo(Set.of(data1, data2));
    }

    @Test
    void testEntrySet() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id1 = Identifiable.Id.of();
        Data data1 = new Data("text", 42);
        Identifiable.Id id2 = Identifiable.Id.of();
        Data data2 = new Data("abc", 21);

        sut.put(id1, data1);
        sut.put(id2, data2);

        // act
        Set<Map.Entry<Identifiable.Id, Data>> retrievedEntries = sut.entrySet();

        // assert
        assertThat(retrievedEntries.size()).isEqualTo(2);
        Set<Identifiable.Id> retrievedKeys = retrievedEntries.stream()
                .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet());
        Set<Data> retrievedValues = retrievedEntries.stream()
                .map(Map.Entry::getValue).collect(Collectors.toSet());

        assertThat(retrievedKeys).isEqualTo(Set.of(id1, id2));
        assertThat(retrievedValues).isEqualTo(Set.of(data1, data2));
    }

    @Test
    void testSize() {
        // arrange
        Random random = new Random();

        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();

        for (int counter=0; counter<100; counter++) {
            Identifiable.Id id = Identifiable.Id.of();
            Data data = new Data("" + random.nextInt(), random.nextInt());
            sut.put(id, data);
        }

        // act
        int expectedSize = sut.size();

        // assert
        assertThat(expectedSize).isEqualTo(100);
    }

    @Test
    void testSizeDoesNotChangeAfterPutWithSameId() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();

        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);
        sut.put(id, data);
        int size = sut.size();
        assertThat(size).isEqualTo(1);

        // act
        sut.put(id, data);
        int expectedSize = sut.size();

        // assert
        assertThat(expectedSize).isEqualTo(size);
    }

    @Test
    void testIsEmptyInitially() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();

        // act
        boolean expectedEmpty = sut.isEmpty();

        // assert
        assertThat(expectedEmpty).isTrue();
    }

    @Test
    void testIsNotEmptyAfterPuttingData() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);
        sut.put(id, data);

        // act
        boolean expectedEmpty = sut.isEmpty();

        // assert
        assertThat(expectedEmpty).isFalse();
    }

    @Test
    void testContains() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);
        sut.put(id, data);

        // act
        boolean expectedContains = sut.contains(data);

        // assert
        assertThat(expectedContains).isTrue();
    }

    @Test
    void testContainsForNotPuttedId() {
        // arrange
        InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
        Identifiable.Id id = Identifiable.Id.of();
        Data data = new Data("text", 42);
        sut.put(id, data);

        // act
        Data newData = new Data("def", 21);
        boolean expectedContains = sut.contains(newData);

        // assert
        assertThat(expectedContains).isFalse();
    }

}