package de.effigenix.application.production;

import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("ListProductionOrders Use Case")
class ListProductionOrdersTest {

    @Mock private ProductionOrderRepository productionOrderRepository;
    @Mock private AuthorizationPort authPort;

    private ListProductionOrders listProductionOrders;
    private ActorId performedBy;

    private static final LocalDate PLANNED_DATE = LocalDate.now().plusDays(7);

    @BeforeEach
    void setUp() {
        listProductionOrders = new ListProductionOrders(productionOrderRepository, authPort);
        performedBy = ActorId.of("admin-user");
    }

    private ProductionOrder sampleOrder() {
        return ProductionOrder.reconstitute(
                ProductionOrderId.of("order-1"),
                RecipeId.of("recipe-1"),
                ProductionOrderStatus.PLANNED,
                null,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                PLANNED_DATE,
                Priority.NORMAL,
                null,
                null,
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                1L
        );
    }

    @Test
    @DisplayName("should list all orders")
    void should_ListAll() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true);
        when(productionOrderRepository.findAll()).thenReturn(Result.success(List.of(sampleOrder())));

        var result = listProductionOrders.execute(performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue()).hasSize(1);
        verify(productionOrderRepository).findAll();
    }

    @Test
    @DisplayName("should delegate to findByDateRange")
    void should_DelegateToFindByDateRange() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true);
        var from = LocalDate.now();
        var to = LocalDate.now().plusDays(30);
        when(productionOrderRepository.findByDateRange(from, to)).thenReturn(Result.success(List.of(sampleOrder())));

        var result = listProductionOrders.executeByDateRange(from, to, performedBy);

        assertThat(result.isSuccess()).isTrue();
        verify(productionOrderRepository).findByDateRange(from, to);
    }

    @Test
    @DisplayName("should delegate to findByStatus")
    void should_DelegateToFindByStatus() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true);
        when(productionOrderRepository.findByStatus(ProductionOrderStatus.PLANNED))
                .thenReturn(Result.success(List.of(sampleOrder())));

        var result = listProductionOrders.executeByStatus(ProductionOrderStatus.PLANNED, performedBy);

        assertThat(result.isSuccess()).isTrue();
        verify(productionOrderRepository).findByStatus(ProductionOrderStatus.PLANNED);
    }

    @Test
    @DisplayName("should delegate to findByDateRangeAndStatus")
    void should_DelegateToFindByDateRangeAndStatus() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true);
        var from = LocalDate.now();
        var to = LocalDate.now().plusDays(30);
        when(productionOrderRepository.findByDateRangeAndStatus(from, to, ProductionOrderStatus.PLANNED))
                .thenReturn(Result.success(List.of(sampleOrder())));

        var result = listProductionOrders.executeByDateRangeAndStatus(from, to, ProductionOrderStatus.PLANNED, performedBy);

        assertThat(result.isSuccess()).isTrue();
        verify(productionOrderRepository).findByDateRangeAndStatus(from, to, ProductionOrderStatus.PLANNED);
    }

    @Test
    @DisplayName("should fail when unauthorized")
    void should_Fail_When_Unauthorized() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(false);

        var result = listProductionOrders.execute(performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.Unauthorized.class);
        verify(productionOrderRepository, never()).findAll();
    }

    @Test
    @DisplayName("should fail when repository returns error")
    void should_Fail_When_RepositoryError() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true);
        when(productionOrderRepository.findAll())
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = listProductionOrders.execute(performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class);
    }
}
