// Copyright © 2016-2018 Esko Luontola
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

package fi.luontola.cqrshotel;

import fi.luontola.cqrshotel.framework.EventStore;
import fi.luontola.cqrshotel.framework.InMemoryEventStore;
import fi.luontola.cqrshotel.framework.consistency.ObservedPosition;
import fi.luontola.cqrshotel.pricing.PricingEngine;
import fi.luontola.cqrshotel.pricing.RandomPricingEngine;
import fi.luontola.cqrshotel.reservation.commands.MakeReservation;
import fi.luontola.cqrshotel.reservation.commands.SearchForAccommodation;
import fi.luontola.cqrshotel.reservation.queries.ReservationOffer;
import fi.luontola.cqrshotel.room.commands.CreateRoom;
import fi.luontola.cqrshotel.room.queries.GetAvailabilityByDateRange;
import fi.luontola.cqrshotel.room.queries.RoomAvailabilityDto;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.ExpectedException;
import org.junit.rules.Timeout;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.UUID;

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

@Category(FastTests.class)
public class AcceptanceTest {

    private static final UUID reservationId = UUID.randomUUID();
    private static final LocalDate arrival = LocalDate.now();
    private static final LocalDate departure = arrival.plusDays(1);

    private final Clock clock = Clock.systemDefaultZone();
    private final EventStore eventStore = new InMemoryEventStore();
    private final PricingEngine pricing = new RandomPricingEngine(clock);
    private final ObservedPosition observedPosition = new ObservedPosition(Duration.ofSeconds(5));
    private final Core core = new Core(eventStore, pricing, clock, observedPosition);

    @Rule
    public final Timeout timeout = Timeout.seconds(2);
    public static final Duration assertionTimeout = Duration.ofSeconds(1);
    @Rule
    public final ExpectedException thrown = ExpectedException.none();

    @Test
    public void making_a_reservation() {
        core.handle(new CreateRoom(UUID.randomUUID(), "101"));
        assertRoomAvailable(true);

        ReservationOffer offer = (ReservationOffer) core.handle(new SearchForAccommodation(reservationId, arrival, departure));
        assertThat("total price", offer.totalPrice, is(notNullValue()));

        core.handle(new MakeReservation(reservationId, arrival, departure, "John Doe", "john@example.com"));
        eventually(() -> assertRoomAvailable(false));
    }


    // helpers

    private void assertRoomAvailable(boolean expected) {
        RoomAvailabilityDto[] rooms = (RoomAvailabilityDto[]) core.handle(new GetAvailabilityByDateRange(arrival, departure));
        assertThat(rooms, is(arrayWithSize(1)));
        assertThat("room available", rooms[0].available, is(expected));
    }

    /**
     * Makes assertions about eventually consistent facts, typically when dealing with process managers.
     */
    private void eventually(Runnable assertion) {
        Instant timeout = Instant.now().plus(assertionTimeout);
        do {
            try {
                assertion.run();
                return;
            } catch (AssertionError e) {
                // make the next query wait for the projection to be updated with new events
                observedPosition.observe(observedPosition.get() + 1);
            }
        } while (Instant.now().isBefore(timeout));
    }
}

@@ -368,24 +368,106 @@
 edPosition;%0A
+import fi.luontola.cqrshotel.framework.consistency.ReadModelNotUpToDateException;%0A
 import fi.lu
@@ -960,16 +960,45 @@
 ityDto;%0A
+import org.hamcrest.Matcher;%0A
 import o
@@ -1543,16 +1543,237 @@
 Test %7B%0A%0A
+    @Rule%0A    public final Timeout timeout = Timeout.seconds(2);%0A    public static final Duration assertionTimeout = Duration.ofSeconds(1);%0A    @Rule%0A    public final ExpectedException thrown = ExpectedException.none();%0A%0A
     priv
@@ -2240,29 +2240,24 @@
 ion(
-Duration.ofSeconds(5)
+assertionTimeout
 );%0A 
@@ -2346,229 +2346,8 @@
 );%0A%0A
-    @Rule%0A    public final Timeout timeout = Timeout.seconds(2);%0A    public static final Duration assertionTimeout = Duration.ofSeconds(1);%0A    @Rule%0A    public final ExpectedException thrown = ExpectedException.none();%0A%0A
     
@@ -2488,13 +2488,39 @@
 ble(
+%22before reservation%22, is(
 true)
+)
 ;%0A%0A 
@@ -2878,15 +2878,40 @@
 ble(
+%22after reservation%22, is(
 false))
+)
 ;%0A  
@@ -2973,24 +2973,48 @@
 ble(
-boolean expected
+String message, Matcher%3CBoolean%3E matcher
 ) %7B%0A
@@ -3223,16 +3223,33 @@
 vailable
+ %22 + message + %22?
 %22, rooms
@@ -3267,20 +3267,15 @@
 le, 
-is(expected)
+matcher
 );%0A 
@@ -3511,24 +3511,70 @@
 onTimeout);%0A
+        AssertionError assertionError = null;%0A
         do %7B
@@ -3849,24 +3849,397 @@
 get() + 1);%0A
+                assertionError = e;%0A            %7D catch (ReadModelNotUpToDateException e) %7B%0A                // assertion timed out while waiting for new events%0A                if (assertionError != null) %7B%0A                    assertionError.addSuppressed(e);%0A                    throw assertionError;%0A                %7D else %7B%0A                    throw e;%0A                %7D%0A
             

Improve error messages