package de.effigenix.domain.production;

import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.UnitOfMeasure;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("ProductionOrder Aggregate")
class ProductionOrderTest {

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

    private ProductionOrderDraft validDraft() {
        return new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "NORMAL", null);
    }

    @Nested
    @DisplayName("create()")
    class Create {

        @Test
        @DisplayName("should create production order with valid draft")
        void should_CreateOrder_When_ValidDraft() {
            var result = ProductionOrder.create(validDraft());

            assertThat(result.isSuccess()).isTrue();
            var order = result.unsafeGetValue();
            assertThat(order.id()).isNotNull();
            assertThat(order.recipeId().value()).isEqualTo("recipe-123");
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.PLANNED);
            assertThat(order.plannedQuantity().amount()).isEqualByComparingTo(new BigDecimal("100"));
            assertThat(order.plannedQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
            assertThat(order.plannedDate()).isEqualTo(FUTURE_DATE);
            assertThat(order.priority()).isEqualTo(Priority.NORMAL);
            assertThat(order.notes()).isNull();
            assertThat(order.createdAt()).isNotNull();
            assertThat(order.updatedAt()).isNotNull();
        }

        @Test
        @DisplayName("should accept notes when provided")
        void should_AcceptNotes_When_Provided() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "HIGH", "Eilauftrag");

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should accept all priority values")
        void should_AcceptAllPriorities() {
            for (Priority p : Priority.values()) {
                var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, p.name(), null);
                var result = ProductionOrder.create(draft);
                assertThat(result.isSuccess()).isTrue();
                assertThat(result.unsafeGetValue().priority()).isEqualTo(p);
            }
        }

        @Test
        @DisplayName("should fail when recipeId is blank")
        void should_Fail_When_RecipeIdBlank() {
            var draft = new ProductionOrderDraft("", "100", "KILOGRAM", FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when recipeId is null")
        void should_Fail_When_RecipeIdNull() {
            var draft = new ProductionOrderDraft(null, "100", "KILOGRAM", FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when plannedQuantity is zero")
        void should_Fail_When_QuantityZero() {
            var draft = new ProductionOrderDraft("recipe-123", "0", "KILOGRAM", FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when plannedQuantity is negative")
        void should_Fail_When_QuantityNegative() {
            var draft = new ProductionOrderDraft("recipe-123", "-5", "KILOGRAM", FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when plannedQuantity is not a number")
        void should_Fail_When_QuantityNotANumber() {
            var draft = new ProductionOrderDraft("recipe-123", "abc", "KILOGRAM", FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when unit is invalid")
        void should_Fail_When_UnitInvalid() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "INVALID_UNIT", FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when plannedDate is yesterday")
        void should_Fail_When_PlannedDateYesterday() {
            var yesterday = LocalDate.now().minusDays(1);
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", yesterday, "NORMAL", null);

            var result = ProductionOrder.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.PlannedDateInPast.class);
            var err = (ProductionOrderError.PlannedDateInPast) result.unsafeGetError();
            assertThat(err.date()).isEqualTo(yesterday);
        }

        @Test
        @DisplayName("should succeed when plannedDate is today")
        void should_Succeed_When_PlannedDateToday() {
            var today = LocalDate.now();
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", today, "NORMAL", null);

            var result = ProductionOrder.create(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().plannedDate()).isEqualTo(today);
        }

        @Test
        @DisplayName("should fail when plannedDate is null")
        void should_Fail_When_PlannedDateNull() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", null, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when priority is invalid")
        void should_Fail_When_PriorityInvalid() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "SUPER_HIGH", null);

            var result = ProductionOrder.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPriority.class);
            var err = (ProductionOrderError.InvalidPriority) result.unsafeGetError();
            assertThat(err.value()).isEqualTo("SUPER_HIGH");
        }

        @Test
        @DisplayName("should fail when priority is null")
        void should_Fail_When_PriorityNull() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, null, null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should accept decimal quantity")
        void should_Accept_When_DecimalQuantity() {
            var draft = new ProductionOrderDraft("recipe-123", "50.75", "LITER", FUTURE_DATE, "LOW", null);

            var result = ProductionOrder.create(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().plannedQuantity().amount())
                    .isEqualByComparingTo(new BigDecimal("50.75"));
            assertThat(result.unsafeGetValue().plannedQuantity().uom()).isEqualTo(UnitOfMeasure.LITER);
        }

        @Test
        @DisplayName("should fail when plannedQuantity is null")
        void should_Fail_When_QuantityNull() {
            var draft = new ProductionOrderDraft("recipe-123", null, "KILOGRAM", FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when plannedQuantityUnit is null")
        void should_Fail_When_UnitNull() {
            var draft = new ProductionOrderDraft("recipe-123", "100", null, FUTURE_DATE, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when plannedDate is far in the past")
        void should_Fail_When_PlannedDateFarInPast() {
            var pastDate = LocalDate.of(2020, 1, 1);
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", pastDate, "NORMAL", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when priority is empty string")
        void should_Fail_When_PriorityEmpty() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should fail when priority is lowercase")
        void should_Fail_When_PriorityLowercase() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "normal", null);

            var result = ProductionOrder.create(draft);

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

        @Test
        @DisplayName("should set version to 0 on creation")
        void should_SetVersionToZero_When_Created() {
            var result = ProductionOrder.create(validDraft());

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().version()).isEqualTo(0L);
        }

        @Test
        @DisplayName("should set createdAt equal to updatedAt on creation")
        void should_SetTimestampsEqual_When_Created() {
            var result = ProductionOrder.create(validDraft());

            assertThat(result.isSuccess()).isTrue();
            var order = result.unsafeGetValue();
            assertThat(order.createdAt()).isEqualTo(order.updatedAt());
        }

        @Test
        @DisplayName("should accept null notes")
        void should_Accept_When_NotesNull() {
            var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "URGENT", null);

            var result = ProductionOrder.create(draft);

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

    @Nested
    @DisplayName("release()")
    class Release {

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

        @Test
        @DisplayName("should release PLANNED order")
        void should_Release_When_Planned() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);
            var beforeUpdate = order.updatedAt();

            var result = order.release();

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.RELEASED);
            assertThat(order.updatedAt()).isAfter(beforeUpdate);
        }

        @Test
        @DisplayName("should fail when releasing RELEASED order")
        void should_Fail_When_AlreadyReleased() {
            var order = orderWithStatus(ProductionOrderStatus.RELEASED);

            var result = order.release();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.RELEASED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.RELEASED);
        }

        @Test
        @DisplayName("should fail when releasing IN_PROGRESS order")
        void should_Fail_When_InProgress() {
            var order = orderWithStatus(ProductionOrderStatus.IN_PROGRESS);

            var result = order.release();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.IN_PROGRESS);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.RELEASED);
        }

        @Test
        @DisplayName("should fail when releasing COMPLETED order")
        void should_Fail_When_Completed() {
            var order = orderWithStatus(ProductionOrderStatus.COMPLETED);

            var result = order.release();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.COMPLETED);
        }

        @Test
        @DisplayName("should fail when releasing CANCELLED order")
        void should_Fail_When_Cancelled() {
            var order = orderWithStatus(ProductionOrderStatus.CANCELLED);

            var result = order.release();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.CANCELLED);
        }
    }

    @Nested
    @DisplayName("startProduction()")
    class StartProduction {

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

        private ProductionOrder releasedOrder() {
            return orderWithStatus(ProductionOrderStatus.RELEASED);
        }

        @Test
        @DisplayName("should start production for RELEASED order")
        void should_StartProduction_When_Released() {
            var order = releasedOrder();
            var beforeUpdate = order.updatedAt();
            var batchId = BatchId.of("batch-1");

            var result = order.startProduction(batchId);

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.IN_PROGRESS);
            assertThat(order.batchId()).isEqualTo(batchId);
            assertThat(order.updatedAt()).isAfter(beforeUpdate);
        }

        @Test
        @DisplayName("should fail when order is PLANNED")
        void should_Fail_When_Planned() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);

            var result = order.startProduction(BatchId.of("batch-1"));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.PLANNED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.IN_PROGRESS);
        }

        @Test
        @DisplayName("should fail when order is IN_PROGRESS")
        void should_Fail_When_InProgress() {
            var order = orderWithStatus(ProductionOrderStatus.IN_PROGRESS);

            var result = order.startProduction(BatchId.of("batch-1"));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.IN_PROGRESS);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.IN_PROGRESS);
        }

        @Test
        @DisplayName("should fail when order is COMPLETED")
        void should_Fail_When_Completed() {
            var order = orderWithStatus(ProductionOrderStatus.COMPLETED);

            var result = order.startProduction(BatchId.of("batch-1"));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.COMPLETED);
        }

        @Test
        @DisplayName("should fail when order is CANCELLED")
        void should_Fail_When_Cancelled() {
            var order = orderWithStatus(ProductionOrderStatus.CANCELLED);

            var result = order.startProduction(BatchId.of("batch-1"));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.CANCELLED);
        }

        @Test
        @DisplayName("should fail when batchId already assigned")
        void should_Fail_When_BatchAlreadyAssigned() {
            var order = ProductionOrder.reconstitute(
                    ProductionOrderId.of("order-1"),
                    RecipeId.of("recipe-123"),
                    ProductionOrderStatus.RELEASED,
                    BatchId.of("existing-batch"),
                    Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    FUTURE_DATE,
                    Priority.NORMAL,
                    null,
                    null,
                    OffsetDateTime.now(ZoneOffset.UTC),
                    OffsetDateTime.now(ZoneOffset.UTC),
                    1L
            );

            var result = order.startProduction(BatchId.of("new-batch"));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.BatchAlreadyAssigned) result.unsafeGetError();
            assertThat(err.batchId().value()).isEqualTo("existing-batch");
        }

        @Test
        @DisplayName("batchId should be null after create")
        void should_HaveNullBatchId_After_Create() {
            var result = ProductionOrder.create(validDraft());

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().batchId()).isNull();
        }
    }

    @Nested
    @DisplayName("complete()")
    class Complete {

        private ProductionOrder orderWithStatus(ProductionOrderStatus status) {
            return ProductionOrder.reconstitute(
                    ProductionOrderId.of("order-1"),
                    RecipeId.of("recipe-123"),
                    status,
                    status == ProductionOrderStatus.IN_PROGRESS || status == ProductionOrderStatus.COMPLETED
                            ? BatchId.of("batch-1") : null,
                    Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    FUTURE_DATE,
                    Priority.NORMAL,
                    null,
                    null,
                    OffsetDateTime.now(ZoneOffset.UTC).minusHours(1),
                    OffsetDateTime.now(ZoneOffset.UTC).minusHours(1),
                    1L
            );
        }

        @Test
        @DisplayName("should complete IN_PROGRESS order")
        void should_Complete_When_InProgress() {
            var order = orderWithStatus(ProductionOrderStatus.IN_PROGRESS);
            var beforeUpdate = order.updatedAt();

            var result = order.complete();

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.COMPLETED);
            assertThat(order.updatedAt()).isAfter(beforeUpdate);
        }

        @Test
        @DisplayName("should fail when completing PLANNED order")
        void should_Fail_When_Planned() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);

            var result = order.complete();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.PLANNED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.COMPLETED);
        }

        @Test
        @DisplayName("should fail when completing RELEASED order")
        void should_Fail_When_Released() {
            var order = orderWithStatus(ProductionOrderStatus.RELEASED);

            var result = order.complete();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.RELEASED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.COMPLETED);
        }

        @Test
        @DisplayName("should fail when completing already COMPLETED order")
        void should_Fail_When_AlreadyCompleted() {
            var order = orderWithStatus(ProductionOrderStatus.COMPLETED);

            var result = order.complete();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.COMPLETED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.COMPLETED);
        }

        @Test
        @DisplayName("should fail when completing CANCELLED order")
        void should_Fail_When_Cancelled() {
            var order = orderWithStatus(ProductionOrderStatus.CANCELLED);

            var result = order.complete();

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.CANCELLED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.COMPLETED);
        }
    }

    @Nested
    @DisplayName("cancel()")
    class Cancel {

        private ProductionOrder orderWithStatus(ProductionOrderStatus status) {
            return ProductionOrder.reconstitute(
                    ProductionOrderId.of("order-1"),
                    RecipeId.of("recipe-123"),
                    status,
                    status == ProductionOrderStatus.IN_PROGRESS || status == ProductionOrderStatus.COMPLETED
                            ? BatchId.of("batch-1") : null,
                    Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    FUTURE_DATE,
                    Priority.NORMAL,
                    null,
                    null,
                    OffsetDateTime.now(ZoneOffset.UTC).minusHours(1),
                    OffsetDateTime.now(ZoneOffset.UTC).minusHours(1),
                    1L
            );
        }

        @Test
        @DisplayName("should cancel PLANNED order with reason")
        void should_Cancel_When_Planned() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);
            var beforeUpdate = order.updatedAt();

            var result = order.cancel("Kunde hat storniert");

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.CANCELLED);
            assertThat(order.cancelledReason()).isEqualTo("Kunde hat storniert");
            assertThat(order.updatedAt()).isAfter(beforeUpdate);
        }

        @Test
        @DisplayName("should cancel RELEASED order with reason")
        void should_Cancel_When_Released() {
            var order = orderWithStatus(ProductionOrderStatus.RELEASED);
            var beforeUpdate = order.updatedAt();

            var result = order.cancel("Material nicht verfügbar");

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.CANCELLED);
            assertThat(order.cancelledReason()).isEqualTo("Material nicht verfügbar");
            assertThat(order.updatedAt()).isAfter(beforeUpdate);
        }

        @Test
        @DisplayName("should fail when reason is blank")
        void should_Fail_When_ReasonBlank() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);

            var result = order.cancel("");

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

        @Test
        @DisplayName("should fail when reason is null")
        void should_Fail_When_ReasonNull() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);

            var result = order.cancel(null);

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

        @Test
        @DisplayName("should fail when cancelling IN_PROGRESS order")
        void should_Fail_When_InProgress() {
            var order = orderWithStatus(ProductionOrderStatus.IN_PROGRESS);

            var result = order.cancel("Storniergrund");

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.IN_PROGRESS);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.CANCELLED);
        }

        @Test
        @DisplayName("should fail when cancelling COMPLETED order")
        void should_Fail_When_Completed() {
            var order = orderWithStatus(ProductionOrderStatus.COMPLETED);

            var result = order.cancel("Storniergrund");

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.COMPLETED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.CANCELLED);
        }

        @Test
        @DisplayName("should fail when cancelling already CANCELLED order")
        void should_Fail_When_AlreadyCancelled() {
            var order = orderWithStatus(ProductionOrderStatus.CANCELLED);

            var result = order.cancel("Storniergrund");

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.InvalidStatusTransition) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.CANCELLED);
            assertThat(err.target()).isEqualTo(ProductionOrderStatus.CANCELLED);
        }
    }

    @Nested
    @DisplayName("reschedule()")
    class Reschedule {

        private ProductionOrder orderWithStatus(ProductionOrderStatus status) {
            return ProductionOrder.reconstitute(
                    ProductionOrderId.of("order-1"),
                    RecipeId.of("recipe-123"),
                    status,
                    status == ProductionOrderStatus.IN_PROGRESS || status == ProductionOrderStatus.COMPLETED
                            ? BatchId.of("batch-1") : null,
                    Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    FUTURE_DATE,
                    Priority.NORMAL,
                    null,
                    null,
                    OffsetDateTime.now(ZoneOffset.UTC).minusHours(1),
                    OffsetDateTime.now(ZoneOffset.UTC).minusHours(1),
                    1L
            );
        }

        @Test
        @DisplayName("should reschedule PLANNED order")
        void should_Reschedule_When_Planned() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);
            var beforeUpdate = order.updatedAt();
            var newDate = LocalDate.now().plusDays(14);

            var result = order.reschedule(newDate);

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.plannedDate()).isEqualTo(newDate);
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.PLANNED);
            assertThat(order.updatedAt()).isAfter(beforeUpdate);
        }

        @Test
        @DisplayName("should reschedule RELEASED order")
        void should_Reschedule_When_Released() {
            var order = orderWithStatus(ProductionOrderStatus.RELEASED);
            var beforeUpdate = order.updatedAt();
            var newDate = LocalDate.now().plusDays(14);

            var result = order.reschedule(newDate);

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.plannedDate()).isEqualTo(newDate);
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.RELEASED);
            assertThat(order.updatedAt()).isAfter(beforeUpdate);
        }

        @Test
        @DisplayName("should accept today as new date")
        void should_Succeed_When_RescheduledToToday() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);
            var today = LocalDate.now();

            var result = order.reschedule(today);

            assertThat(result.isSuccess()).isTrue();
            assertThat(order.plannedDate()).isEqualTo(today);
        }

        @Test
        @DisplayName("should fail when rescheduling IN_PROGRESS order")
        void should_Fail_When_InProgress() {
            var order = orderWithStatus(ProductionOrderStatus.IN_PROGRESS);

            var result = order.reschedule(LocalDate.now().plusDays(14));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.RescheduleNotAllowed) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.IN_PROGRESS);
        }

        @Test
        @DisplayName("should fail when rescheduling COMPLETED order")
        void should_Fail_When_Completed() {
            var order = orderWithStatus(ProductionOrderStatus.COMPLETED);

            var result = order.reschedule(LocalDate.now().plusDays(14));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.RescheduleNotAllowed) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.COMPLETED);
        }

        @Test
        @DisplayName("should fail when rescheduling CANCELLED order")
        void should_Fail_When_Cancelled() {
            var order = orderWithStatus(ProductionOrderStatus.CANCELLED);

            var result = order.reschedule(LocalDate.now().plusDays(14));

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.RescheduleNotAllowed) result.unsafeGetError();
            assertThat(err.current()).isEqualTo(ProductionOrderStatus.CANCELLED);
        }

        @Test
        @DisplayName("should fail when newDate is null")
        void should_Fail_When_NewDateNull() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);

            var result = order.reschedule(null);

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

        @Test
        @DisplayName("should fail when new date is in the past")
        void should_Fail_When_DateInPast() {
            var order = orderWithStatus(ProductionOrderStatus.PLANNED);
            var yesterday = LocalDate.now().minusDays(1);

            var result = order.reschedule(yesterday);

            assertThat(result.isFailure()).isTrue();
            var err = (ProductionOrderError.PlannedDateInPast) result.unsafeGetError();
            assertThat(err.date()).isEqualTo(yesterday);
        }

        @Test
        @DisplayName("should not change status on reschedule")
        void should_NotChangeStatus_When_Rescheduled() {
            var order = orderWithStatus(ProductionOrderStatus.RELEASED);

            order.reschedule(LocalDate.now().plusDays(30));

            assertThat(order.status()).isEqualTo(ProductionOrderStatus.RELEASED);
        }
    }

    @Nested
    @DisplayName("reconstitute()")
    class Reconstitute {

        @Test
        @DisplayName("should reconstitute production order from persistence")
        void should_Reconstitute_When_ValidData() {
            var order = ProductionOrder.reconstitute(
                    ProductionOrderId.of("order-1"),
                    RecipeId.of("recipe-123"),
                    ProductionOrderStatus.PLANNED,
                    null,
                    Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    FUTURE_DATE,
                    Priority.HIGH,
                    "Wichtiger Auftrag",
                    null,
                    OffsetDateTime.now(ZoneOffset.UTC),
                    OffsetDateTime.now(ZoneOffset.UTC),
                    5L
            );

            assertThat(order.id().value()).isEqualTo("order-1");
            assertThat(order.recipeId().value()).isEqualTo("recipe-123");
            assertThat(order.status()).isEqualTo(ProductionOrderStatus.PLANNED);
            assertThat(order.priority()).isEqualTo(Priority.HIGH);
            assertThat(order.notes()).isEqualTo("Wichtiger Auftrag");
            assertThat(order.version()).isEqualTo(5L);
        }
    }
}
