package de.effigenix.application.production;

import de.effigenix.application.production.command.CreateProductionOrderCommand;
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 de.effigenix.shared.persistence.UnitOfWork;
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("CreateProductionOrder Use Case")
class CreateProductionOrderTest {

    @Mock private ProductionOrderRepository productionOrderRepository;
    @Mock private RecipeRepository recipeRepository;
    @Mock private AuthorizationPort authPort;
    @Mock private UnitOfWork unitOfWork;

    private CreateProductionOrder createProductionOrder;
    private ActorId performedBy;

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

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

    private CreateProductionOrderCommand validCommand() {
        return new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
    }

    private Recipe activeRecipe() {
        return Recipe.reconstitute(
                RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
                null, new YieldPercentage(85), 14,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
        );
    }

    private Recipe draftRecipe() {
        return Recipe.reconstitute(
                RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
                null, new YieldPercentage(85), 14,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                "article-123", RecipeStatus.DRAFT, List.of(), List.of(),
                OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
        );
    }

    @Test
    @DisplayName("should create production order when recipe is ACTIVE and all data valid")
    void should_CreateOrder_When_ValidCommand() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(activeRecipe())));
        when(productionOrderRepository.save(any())).thenReturn(Result.success(null));

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

        assertThat(result.isSuccess()).isTrue();
        var order = result.unsafeGetValue();
        assertThat(order.status()).isEqualTo(ProductionOrderStatus.PLANNED);
        assertThat(order.recipeId().value()).isEqualTo("recipe-1");
        assertThat(order.priority()).isEqualTo(Priority.NORMAL);
        verify(productionOrderRepository).save(any(ProductionOrder.class));
    }

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

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

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

    @Test
    @DisplayName("should fail when recipe not found")
    void should_Fail_When_RecipeNotFound() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.empty()));

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

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class);
        assertThat(result.unsafeGetError().message()).contains("recipe-1");
        verify(productionOrderRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail when recipe is not ACTIVE")
    void should_Fail_When_RecipeNotActive() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(draftRecipe())));

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

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

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

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

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

    @Test
    @DisplayName("should fail when production order repository save fails")
    void should_Fail_When_SaveFails() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(activeRecipe())));
        when(productionOrderRepository.save(any()))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

    @Test
    @DisplayName("should fail when planned quantity is invalid")
    void should_Fail_When_InvalidQuantity() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(activeRecipe())));

        var cmd = new CreateProductionOrderCommand("recipe-1", "0", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
        var result = createProductionOrder.execute(cmd, performedBy);

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

    @Test
    @DisplayName("should fail when planned date is in the past")
    void should_Fail_When_PlannedDateInPast() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(activeRecipe())));

        var cmd = new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM",
                LocalDate.now().minusDays(1), "NORMAL", null);
        var result = createProductionOrder.execute(cmd, performedBy);

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

    @Test
    @DisplayName("should fail when priority is invalid")
    void should_Fail_When_InvalidPriority() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(activeRecipe())));

        var cmd = new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM", PLANNED_DATE, "SUPER_HIGH", null);
        var result = createProductionOrder.execute(cmd, performedBy);

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

    @Test
    @DisplayName("should fail when unit is invalid")
    void should_Fail_When_InvalidUnit() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(activeRecipe())));

        var cmd = new CreateProductionOrderCommand("recipe-1", "100", "INVALID", PLANNED_DATE, "NORMAL", null);
        var result = createProductionOrder.execute(cmd, performedBy);

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

    @Test
    @DisplayName("should pass notes through to production order")
    void should_PassNotes_When_Provided() {
        when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("recipe-1")))
                .thenReturn(Result.success(Optional.of(activeRecipe())));
        when(productionOrderRepository.save(any())).thenReturn(Result.success(null));

        var cmd = new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM", PLANNED_DATE, "URGENT", "Eilauftrag");
        var result = createProductionOrder.execute(cmd, performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().notes()).isEqualTo("Eilauftrag");
        assertThat(result.unsafeGetValue().priority()).isEqualTo(Priority.URGENT);
    }
}
