package de.effigenix.application.production;

import de.effigenix.application.production.command.CompleteProductionOrderCommand;
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.persistence.UnitOfWork;
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 java.util.Optional;
import java.util.function.Supplier;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("CompleteProductionOrder Use Case")
class CompleteProductionOrderTest {

    @Mock private ProductionOrderRepository productionOrderRepository;
    @Mock private BatchRepository batchRepository;
    @Mock private AuthorizationPort authPort;
    @Mock private UnitOfWork unitOfWork;

    private CompleteProductionOrder completeProductionOrder;
    private ActorId performedBy;

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

    @BeforeEach
    void setUp() {
        completeProductionOrder = new CompleteProductionOrder(productionOrderRepository, batchRepository, authPort, unitOfWork);
        performedBy = ActorId.of("admin-user");
        lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
    }

    private CompleteProductionOrderCommand validCommand() {
        return new CompleteProductionOrderCommand("order-1");
    }

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

    private ProductionOrder plannedOrder() {
        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
        );
    }

    private Batch completedBatch() {
        return Batch.reconstitute(
                BatchId.of("batch-1"),
                new BatchNumber("P-2026-02-25-001"),
                RecipeId.of("recipe-1"),
                BatchStatus.COMPLETED,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                Quantity.of(new BigDecimal("95"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                Quantity.of(new BigDecimal("5"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                "done",
                PLANNED_DATE, PLANNED_DATE.plusDays(14),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                null, null,
                1L,
                List.of()
        );
    }

    private Batch inProductionBatch() {
        return Batch.reconstitute(
                BatchId.of("batch-1"),
                new BatchNumber("P-2026-02-25-001"),
                RecipeId.of("recipe-1"),
                BatchStatus.IN_PRODUCTION,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                null, null, null,
                PLANNED_DATE, PLANNED_DATE.plusDays(14),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                null, null, null,
                1L,
                List.of()
        );
    }

    @Test
    @DisplayName("should complete order when IN_PROGRESS and batch is COMPLETED")
    void should_Complete_When_InProgressAndBatchCompleted() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(productionOrderRepository.findById(ProductionOrderId.of("order-1")))
                .thenReturn(Result.success(Optional.of(inProgressOrder())));
        when(batchRepository.findById(BatchId.of("batch-1")))
                .thenReturn(Result.success(Optional.of(completedBatch())));
        when(productionOrderRepository.save(any())).thenReturn(Result.success(null));

        var result = completeProductionOrder.execute(validCommand(), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().status()).isEqualTo(ProductionOrderStatus.COMPLETED);
        verify(productionOrderRepository).save(any(ProductionOrder.class));
    }

    @Test
    @DisplayName("should fail when batch is not COMPLETED")
    void should_Fail_When_BatchNotCompleted() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(productionOrderRepository.findById(ProductionOrderId.of("order-1")))
                .thenReturn(Result.success(Optional.of(inProgressOrder())));
        when(batchRepository.findById(BatchId.of("batch-1")))
                .thenReturn(Result.success(Optional.of(inProductionBatch())));

        var result = completeProductionOrder.execute(validCommand(), performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.BatchNotCompleted.class);
        verify(productionOrderRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail when order is PLANNED (no batch assigned)")
    void should_Fail_When_OrderPlanned() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(productionOrderRepository.findById(ProductionOrderId.of("order-1")))
                .thenReturn(Result.success(Optional.of(plannedOrder())));

        var result = completeProductionOrder.execute(validCommand(), performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class);
        verify(productionOrderRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail when order is already COMPLETED")
    void should_Fail_When_AlreadyCompleted() {
        var completedOrder = ProductionOrder.reconstitute(
                ProductionOrderId.of("order-1"), RecipeId.of("recipe-1"),
                ProductionOrderStatus.COMPLETED, BatchId.of("batch-1"),
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                PLANNED_DATE, Priority.NORMAL, null, null,
                OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC), 1L);

        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(productionOrderRepository.findById(ProductionOrderId.of("order-1")))
                .thenReturn(Result.success(Optional.of(completedOrder)));
        when(batchRepository.findById(BatchId.of("batch-1")))
                .thenReturn(Result.success(Optional.of(completedBatch())));

        var result = completeProductionOrder.execute(validCommand(), performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidStatusTransition.class);
        verify(productionOrderRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail when order is CANCELLED")
    void should_Fail_When_OrderCancelled() {
        var cancelledOrder = ProductionOrder.reconstitute(
                ProductionOrderId.of("order-1"), RecipeId.of("recipe-1"),
                ProductionOrderStatus.CANCELLED, null,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                PLANNED_DATE, Priority.NORMAL, null, "Storniert",
                OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC), 1L);

        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(productionOrderRepository.findById(ProductionOrderId.of("order-1")))
                .thenReturn(Result.success(Optional.of(cancelledOrder)));

        var result = completeProductionOrder.execute(validCommand(), performedBy);

        assertThat(result.isFailure()).isTrue();
        // CANCELLED order has no batchId → ValidationFailure
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class);
        verify(productionOrderRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail when order not found")
    void should_Fail_When_OrderNotFound() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(productionOrderRepository.findById(ProductionOrderId.of("order-1")))
                .thenReturn(Result.success(Optional.empty()));

        var result = completeProductionOrder.execute(validCommand(), performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ProductionOrderNotFound.class);
        verify(productionOrderRepository, never()).save(any());
    }

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

        var result = completeProductionOrder.execute(validCommand(), performedBy);

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

    @Test
    @DisplayName("should fail when order repository returns error")
    void should_Fail_When_RepositoryError() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(productionOrderRepository.findById(ProductionOrderId.of("order-1")))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = completeProductionOrder.execute(validCommand(), performedBy);

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