package io.loom.core.event;

import io.loom.core.entity.VersionedEntity;

import java.time.ZonedDateTime;
import java.util.Random;
import java.util.UUID;

import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;

public class AbstractDomainEventSpecs {
    public class IssueCreatedForTesting extends AbstractDomainEvent {
        public IssueCreatedForTesting() {
        }

        public IssueCreatedForTesting(
                UUID aggregateId, long version, ZonedDateTime occurrenceTime) {
            super(aggregateId, version, occurrenceTime);
        }
    }

    @Test
    public void constructor_has_guard_clause_for_null_aggregateId() {
        // Arrange
        UUID aggregateId = null;

        // Act
        IllegalArgumentException expected = null;
        try {
            new IssueCreatedForTesting(aggregateId, 1, ZonedDateTime.now());
        } catch (IllegalArgumentException e) {
            expected = e;
        }

        // Assert
        Assert.assertNotNull(expected);
        Assert.assertTrue(
                "The error message should contain the name of the parameter 'aggregateId'.",
                expected.getMessage().contains("'aggregateId'"));
    }

    @Test
    public void constructor_has_guard_clause_for_minimum_value_of_version() {
        // Arrange
        long version = 0;

        // Act
        IllegalArgumentException expected = null;
        try {
            new IssueCreatedForTesting(UUID.randomUUID(), version, ZonedDateTime.now());
        } catch (IllegalArgumentException e) {
            expected = e;
        }

        // Assert
        Assert.assertNotNull(expected);
        Assert.assertTrue(
                "The error message should contain the name of the parameter 'version'.",
                expected.getMessage().contains("'version'"));
    }

    @Test
    public void constructor_has_guard_clause_for_null_occurrenceTime() {
        // Arrange
        ZonedDateTime occurrenceTime = null;

        // Act
        IllegalArgumentException expected = null;
        try {
            new IssueCreatedForTesting(UUID.randomUUID(), 1, occurrenceTime);
        } catch (IllegalArgumentException e) {
            expected = e;
        }

        // Assert
        Assert.assertNotNull(expected);
        Assert.assertTrue(
                "The error message should contain the name of the parameter 'occurrenceTime'.",
                expected.getMessage().contains("'occurrenceTime'"));
    }

    @Test
    public void constructor_sets_header_properties_correctly() {
        // Arrange
        UUID aggregateId = UUID.randomUUID();
        Random random = new Random();
        long version = random.nextInt(Integer.MAX_VALUE) + 1L;
        ZonedDateTime occurrenceTime = ZonedDateTime.now().plusNanos(random.nextInt());

        // Act
        IssueCreatedForTesting sut = new IssueCreatedForTesting(
                aggregateId, version, occurrenceTime);

        // Assert
        Assert.assertEquals(aggregateId, sut.getAggregateId());
        Assert.assertEquals(version, sut.getVersion());
        Assert.assertEquals(occurrenceTime, sut.getOccurrenceTime());
    }

    @Test
    public void onRaise_has_guard_clause_for_null_aggregateId() {
        // Arrange
        VersionedEntity versionedEntity = Mockito.mock(VersionedEntity.class);
        Mockito.when(versionedEntity.getId()).thenReturn(null);
        Mockito.when(versionedEntity.getVersion()).thenReturn(1L);

        IssueCreatedForTesting sut = new IssueCreatedForTesting();

        // Act
        IllegalArgumentException expected = null;
        try {
            sut.onRaise(versionedEntity);
        } catch (IllegalArgumentException e) {
            expected = e;
        }

        // Assert
        Assert.assertNotNull(expected);
        Assert.assertTrue(
                "The error message should contain the name of the parameter 'aggregateId'.",
                expected.getMessage().contains("'aggregateId'"));
    }

    @Test
    public void onRaise_has_guard_clause_for_minimum_value_of_version() {
        // Arrange
        VersionedEntity versionedEntity = Mockito.mock(VersionedEntity.class);
        Mockito.when(versionedEntity.getId()).thenReturn(UUID.randomUUID());
        Mockito.when(versionedEntity.getVersion()).thenReturn(-1L);

        IssueCreatedForTesting sut = new IssueCreatedForTesting();

        // Act
        IllegalArgumentException expected = null;
        try {
            sut.onRaise(versionedEntity);
        } catch (IllegalArgumentException e) {
            expected = e;
        }

        // Assert
        Assert.assertNotNull(expected);
        Assert.assertTrue(
                "The error message should contain the name of the parameter 'version'.",
                expected.getMessage().contains("'version'"));
    }

    @Test
    public void onRaise_sets_header_properties_correctly() {
        // Arrange
        UUID aggregateId = UUID.randomUUID();
        Random random = new Random();
        long version = random.nextInt(Integer.MAX_VALUE) + 1L;
        VersionedEntity versionedEntity = Mockito.mock(VersionedEntity.class);
        Mockito.when(versionedEntity.getId()).thenReturn(aggregateId);
        Mockito.when(versionedEntity.getVersion()).thenReturn(version);

        IssueCreatedForTesting sut = new IssueCreatedForTesting();

        // Act
        sut.onRaise(versionedEntity);

        // Assert
        Assert.assertEquals(aggregateId, sut.getAggregateId());
        Assert.assertEquals(version + 1, sut.getVersion());
        long occurrenceTime = sut.getOccurrenceTime().toInstant().toEpochMilli();
        long after = ZonedDateTime.now().toInstant().toEpochMilli();
        long before = after - 1000;
        Assert.assertTrue(after >= occurrenceTime);
        Assert.assertTrue(before <= occurrenceTime);
    }

    @Test
    public void onRaise_equals_entity() {
        // Arrange
        VersionedEntity versionedEntity = Mockito.mock(VersionedEntity.class);
        Mockito.when(versionedEntity.getId()).thenReturn(UUID.randomUUID());
        Mockito.when(versionedEntity.getVersion())
                .thenReturn(new Random().nextInt(Integer.MAX_VALUE) + 1L);

        ZonedDateTime occurrenceTime = ZonedDateTime.now();
        IssueCreatedForTesting sut = new IssueCreatedForTesting(
                versionedEntity.getId(), versionedEntity.getVersion() + 1, occurrenceTime);

        // Act
        sut.onRaise(versionedEntity);

        // Assert
        Assert.assertEquals(versionedEntity.getId(), sut.getAggregateId());
        Assert.assertEquals(versionedEntity.getVersion() + 1, sut.getVersion());
        Assert.assertEquals(occurrenceTime, sut.getOccurrenceTime());
    }

    @Test
    public void onRaise_different_id_entity() {
        // Arrange
        UUID aggregateId = UUID.randomUUID();
        Random random = new Random();
        long version = random.nextInt(Integer.MAX_VALUE) + 1L;
        ZonedDateTime occurrenceTime = ZonedDateTime.now();
        IssueCreatedForTesting sut = new IssueCreatedForTesting(
                aggregateId, version, occurrenceTime);

        VersionedEntity otherVersionedEntity = Mockito.mock(VersionedEntity.class);
        Mockito.when(otherVersionedEntity.getId()).thenReturn(UUID.randomUUID());
        Mockito.when(otherVersionedEntity.getVersion()).thenReturn(version);

        IllegalArgumentException expected = null;
        try {
            sut.onRaise(otherVersionedEntity);
        } catch (IllegalArgumentException e) {
            expected = e;
        }

        // Assert
        Assert.assertNotNull(expected);
        Assert.assertTrue(
                "The error message should contain the value of the 'aggregateId'.",
                expected.getMessage().contains(sut.getAggregateId().toString()));
        Assert.assertTrue(
                "The error message should contain the value of the 'version'.",
                expected.getMessage().contains(String.valueOf(sut.getVersion())));
        Assert.assertTrue(
                "The error message should contain the value of other versionedEntity 'id'.",
                expected.getMessage().contains(otherVersionedEntity.getId().toString()));
    }

    @Test
    public void onRaise_different_version_entity() {
        // Arrange
        UUID aggregateId = UUID.randomUUID();
        Random random = new Random();
        long version = random.nextInt(Integer.MAX_VALUE) + 1L;
        ZonedDateTime occurrenceTime = ZonedDateTime.now();
        IssueCreatedForTesting sut = new IssueCreatedForTesting(
                aggregateId, version, occurrenceTime);

        VersionedEntity otherVersionedEntity = Mockito.mock(VersionedEntity.class);
        Mockito.when(otherVersionedEntity.getId()).thenReturn(aggregateId);
        Mockito.when(otherVersionedEntity.getVersion())
                .thenReturn(version + random.nextInt(100) + 1L);

        IllegalArgumentException expected = null;
        try {
            sut.onRaise(otherVersionedEntity);
        } catch (IllegalArgumentException e) {
            expected = e;
        }

        // Assert
        Assert.assertNotNull(expected);
        Assert.assertTrue(
                "The error message should contain the value of the 'aggregateId'.",
                expected.getMessage().contains(sut.getAggregateId().toString()));
        Assert.assertTrue(
                "The error message should contain the value of the 'version'.",
                expected.getMessage().contains(String.valueOf(sut.getVersion())));
        Assert.assertTrue(
                "The error message should contain the value of other "
                         + "'versionedEntity.getVersion() + 1'.",
                expected.getMessage().contains(
                        String.valueOf(otherVersionedEntity.getVersion() + 1)));
    }
}