package com.mattstine.dddworkshop.pizzashop.kitchen;

import com.mattstine.dddworkshop.pizzashop.infrastructure.domain.services.RefStringGenerator;
import com.mattstine.dddworkshop.pizzashop.infrastructure.events.adapters.InProcessEventLog;
import com.mattstine.dddworkshop.pizzashop.infrastructure.events.ports.Topic;
import com.mattstine.dddworkshop.pizzashop.kitchen.acl.ordering.OnlineOrder;
import com.mattstine.dddworkshop.pizzashop.kitchen.acl.ordering.OnlineOrderPaidEvent;
import com.mattstine.dddworkshop.pizzashop.kitchen.acl.ordering.OnlineOrderRef;
import com.mattstine.dddworkshop.pizzashop.kitchen.acl.ordering.OrderingService;
import org.h2.jdbcx.JdbcConnectionPool;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class DefaultKitchenServiceWithJdbcIntegrationTests {

    private InProcessEventLog eventLog;
    private JdbcConnectionPool pool;
    private KitchenService kitchenService;
    private OrderingService orderingService;
    private KitchenOrderRepository kitchenOrderRepository;
    private PizzaRepository pizzaRepository;
    private KitchenOrderRef kitchenOrderRef;
    private KitchenOrder kitchenOrder;

    @Before
    public void setUp() {
        eventLog = InProcessEventLog.instance();
        pool = JdbcConnectionPool.create("jdbc:h2:mem:test;MVCC=FALSE", "", "");
        kitchenOrderRepository = new EmbeddedJdbcKitchenOrderRepository(eventLog,
                new Topic("kitchen_orders"), pool);
        pizzaRepository = new EmbeddedJdbcPizzaRepository(eventLog,
                new Topic("pizzas"), pool);
        orderingService = mock(OrderingService.class);
        kitchenService = new DefaultKitchenService(eventLog, kitchenOrderRepository, pizzaRepository, orderingService);
        kitchenOrderRef = kitchenOrderRepository.nextIdentity();
        kitchenOrder = KitchenOrder.builder()
                .ref(kitchenOrderRef)
                .onlineOrderRef(new OnlineOrderRef(RefStringGenerator.generateRefString()))
                .eventLog(eventLog)
                .pizza(KitchenOrder.Pizza.builder().size(KitchenOrder.Pizza.Size.MEDIUM).build())
                .pizza(KitchenOrder.Pizza.builder().size(KitchenOrder.Pizza.Size.LARGE).build())
                .build();
        kitchenOrderRepository.add(kitchenOrder);
    }

    @After
    public void tearDown() throws SQLException {
        Connection connection = pool.getConnection();
        PreparedStatement statement = connection.prepareStatement("DROP ALL OBJECTS");
        statement.execute();
        connection.close();

        pool.dispose();
        this.eventLog.purgeSubscribers();
    }

    @Test
    public void on_orderPaidEvent_add_to_queue() {
        OnlineOrderRef ref = new OnlineOrderRef(RefStringGenerator.generateRefString());
        OnlineOrderPaidEvent orderPaidEvent = new OnlineOrderPaidEvent(ref);

        OnlineOrder onlineOrder = OnlineOrder.builder()
                .type(OnlineOrder.Type.PICKUP)
                .eventLog(eventLog)
                .ref(ref)
                .build();

        onlineOrder.addPizza(com.mattstine.dddworkshop.pizzashop.kitchen.acl.ordering.Pizza.builder()
                .size(com.mattstine.dddworkshop.pizzashop.kitchen.acl.ordering.Pizza.Size.MEDIUM)
                .build());

        when(orderingService.findByRef(eq(ref))).thenReturn(onlineOrder);

        eventLog.publish(new Topic("ordering"), orderPaidEvent);

        KitchenOrder kitchenOrder = kitchenService.findKitchenOrderByOnlineOrderRef(ref);
        assertThat(kitchenOrder).isNotNull();
    }

    @Test
    public void on_kitchenOrderPrepStartedEvent_start_prep_on_all_pizzas() {
        kitchenOrder.startPrep();

        Set<Pizza> pizzas = kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef);

        assertThat(pizzas.size()).isEqualTo(2);

        assertThat(pizzas.stream()
                .filter(pizza -> pizza.getSize() == Pizza.Size.MEDIUM)
                .count()).isEqualTo(1);

        assertThat(pizzas.stream()
                .filter(pizza -> pizza.getSize() == Pizza.Size.LARGE)
                .count()).isEqualTo(1);

        assertThat(pizzas.stream()
                .filter(pizza -> pizza.getState() == Pizza.State.PREPPING)
                .count()).isEqualTo(2);
    }

    @SuppressWarnings("OptionalGetWithoutIsPresent")
    @Test
    public void on_pizzaPrepFinished_start_pizzaBake() {
        kitchenOrder.startPrep();

        Set<Pizza> pizzasByKitchenOrderRef = kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef);

        Pizza pizza = pizzasByKitchenOrderRef.stream()
                .findFirst()
                .get();

        pizza.finishPrep();

        pizza = pizzaRepository.findByRef(pizza.getRef());
        assertThat(pizza.isBaking()).isTrue();
    }

    @Test
    public void on_pizzaBakeStarted_start_orderBake() {
        kitchenOrder.startPrep();

        Set<Pizza> pizzasByKitchenOrderRef = kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef);

        pizzasByKitchenOrderRef.forEach(Pizza::finishPrep);

        kitchenOrder = kitchenOrderRepository.findByRef(kitchenOrderRef);
        assertThat(kitchenOrder.isBaking()).isTrue();
    }

    @Test
    public void on_first_pizzaBakeFinished_start_orderAssembly() {
        kitchenOrder.startPrep();

        // Load pizzas that are prepping...
        Set<Pizza> pizzasByKitchenOrderRef = kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef);
        pizzasByKitchenOrderRef.forEach(Pizza::finishPrep);

        // Load pizzas that are baking...
        pizzasByKitchenOrderRef = kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef);
        pizzasByKitchenOrderRef.stream()
                .findFirst()
                .ifPresent(Pizza::finishBake);

        // Ensure order has started assembly...
        kitchenOrder = kitchenOrderRepository.findByRef(kitchenOrderRef);
        assertThat(kitchenOrder.hasStartedAssembly()).isTrue();
    }

    @Test
    public void should_start_kitchenOrder_prep() {
        kitchenService.startOrderPrep(kitchenOrderRef);
        kitchenOrder = kitchenService.findKitchenOrderByRef(kitchenOrderRef);
        assertThat(kitchenOrder.isPrepping()).isTrue();
    }

    @SuppressWarnings("OptionalGetWithoutIsPresent")
    @Test
    public void should_finish_pizza_prep() {
        kitchenOrder.startPrep();

        Pizza pizza = kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef).stream()
                .findFirst().get();

        PizzaRef ref = pizza.getRef();
        kitchenService.finishPizzaPrep(ref);
        pizza = kitchenService.findPizzaByRef(ref);

        assertThat(pizza.isBaking()).isTrue();
    }

    @SuppressWarnings("OptionalGetWithoutIsPresent")
    @Test
    public void should_remove_pizza_from_oven() {
        kitchenOrder.startPrep();

        Pizza pizza = kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef).stream()
                .findFirst().get();

        pizza.finishPrep();

        kitchenService.removePizzaFromOven(pizza.getRef());

        pizza = kitchenService.findPizzaByRef(pizza.getRef());

        assertThat(pizza.hasFinishedBaking()).isTrue();
    }

    @Test
    public void on_final_pizzaBakeFinished_finish_orderAssembly() {
        kitchenOrder.startPrep();

        kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef)
                .forEach(Pizza::finishPrep);

        kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef)
                .forEach(pizza -> kitchenService.removePizzaFromOven(pizza.getRef()));

        kitchenOrder = kitchenService.findKitchenOrderByRef(kitchenOrderRef);

        assertThat(kitchenOrder.hasFinishedAssembly()).isTrue();
    }

    @Test
    public void test_bug_only_one_pizza_would_never_finish() {
        kitchenOrderRef = kitchenOrderRepository.nextIdentity();

        kitchenOrder = KitchenOrder.builder()
                .ref(kitchenOrderRef)
                .onlineOrderRef(new OnlineOrderRef())
                .eventLog(eventLog)
                .pizza(KitchenOrder.Pizza.builder().size(KitchenOrder.Pizza.Size.MEDIUM).build())
                .build();
        kitchenOrderRepository.add(kitchenOrder);


        kitchenOrder.startPrep();

        kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef)
                .forEach(Pizza::finishPrep);

        kitchenService.findPizzasByKitchenOrderRef(kitchenOrderRef)
                .forEach(pizza -> kitchenService.removePizzaFromOven(pizza.getRef()));

        kitchenOrder = kitchenService.findKitchenOrderByRef(kitchenOrderRef);

        assertThat(kitchenOrder.hasFinishedAssembly()).isTrue();
    }
}