package de.effigenix.domain.inventory;

import de.effigenix.domain.masterdata.article.ArticleId;
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.Instant;

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

class StockMovementTest {

    private StockMovementDraft validDraft(String movementType, String direction) {
        return new StockMovementDraft(
                "stock-1", "article-1", "batch-id-1",
                "CHARGE-001", "PRODUCED",
                movementType, direction,
                "10.5", "KILOGRAM",
                null, null,
                "user-1"
        );
    }

    private StockMovementDraft validDraft(String movementType) {
        return validDraft(movementType, null);
    }

    // ==================== Factory: record() ====================

    @Nested
    @DisplayName("record() – Positive Cases")
    class PositiveCases {

        @Test
        @DisplayName("should record GOODS_RECEIPT with direction IN")
        void shouldRecordGoodsReceipt() {
            var result = StockMovement.record(validDraft("GOODS_RECEIPT"));

            assertThat(result.isSuccess()).isTrue();
            var movement = result.unsafeGetValue();
            assertThat(movement.id()).isNotNull();
            assertThat(movement.movementType()).isEqualTo(MovementType.GOODS_RECEIPT);
            assertThat(movement.direction()).isEqualTo(MovementDirection.IN);
            assertThat(movement.isIncoming()).isTrue();
            assertThat(movement.isOutgoing()).isFalse();
            assertThat(movement.quantity().amount()).isEqualByComparingTo(new BigDecimal("10.5"));
        }

        @Test
        @DisplayName("should record PRODUCTION_OUTPUT with direction IN")
        void shouldRecordProductionOutput() {
            var result = StockMovement.record(validDraft("PRODUCTION_OUTPUT"));

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.IN);
        }

        @Test
        @DisplayName("should record RETURN with direction IN")
        void shouldRecordReturn() {
            var result = StockMovement.record(validDraft("RETURN"));

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.IN);
        }

        @Test
        @DisplayName("should record PRODUCTION_CONSUMPTION with direction OUT")
        void shouldRecordProductionConsumption() {
            var result = StockMovement.record(validDraft("PRODUCTION_CONSUMPTION"));

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.OUT);
        }

        @Test
        @DisplayName("should record SALE with direction OUT")
        void shouldRecordSale() {
            var result = StockMovement.record(validDraft("SALE"));

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.OUT);
        }

        @Test
        @DisplayName("should record WASTE with reason and direction OUT")
        void shouldRecordWaste() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "WASTE", null,
                    "5.0", "KILOGRAM",
                    "Expired product", null,
                    "user-1"
            );
            var result = StockMovement.record(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.OUT);
            assertThat(result.unsafeGetValue().reason()).isEqualTo("Expired product");
        }

        @Test
        @DisplayName("should record ADJUSTMENT IN with reason")
        void shouldRecordAdjustmentIn() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "ADJUSTMENT", "IN",
                    "2.0", "KILOGRAM",
                    "Inventory count correction", null,
                    "user-1"
            );
            var result = StockMovement.record(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.IN);
            assertThat(result.unsafeGetValue().reason()).isEqualTo("Inventory count correction");
        }

        @Test
        @DisplayName("should record ADJUSTMENT OUT with reason")
        void shouldRecordAdjustmentOut() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "ADJUSTMENT", "OUT",
                    "1.0", "KILOGRAM",
                    "Inventory count correction", null,
                    "user-1"
            );
            var result = StockMovement.record(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.OUT);
        }

        @Test
        @DisplayName("should record INTER_BRANCH_TRANSFER with referenceDocumentId")
        void shouldRecordInterBranchTransfer() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "INTER_BRANCH_TRANSFER", null,
                    "3.0", "KILOGRAM",
                    null, "TRANSFER-001",
                    "user-1"
            );
            var result = StockMovement.record(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.OUT);
            assertThat(result.unsafeGetValue().referenceDocumentId()).isEqualTo("TRANSFER-001");
        }

        @Test
        @DisplayName("should ignore direction field for non-ADJUSTMENT types")
        void shouldIgnoreDirectionForNonAdjustment() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", "OUT",
                    "10.0", "KILOGRAM",
                    null, null,
                    "user-1"
            );
            var result = StockMovement.record(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().direction()).isEqualTo(MovementDirection.IN);
        }
    }

    // ==================== Factory: record() – Negative Cases ====================

    @Nested
    @DisplayName("record() – Validation Errors")
    class ValidationErrors {

        @Test
        @DisplayName("should fail when stockId is null")
        void shouldFailWhenStockIdNull() {
            var draft = new StockMovementDraft(
                    null, "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockId.class);
        }

        @Test
        @DisplayName("should fail when stockId is blank")
        void shouldFailWhenStockIdBlank() {
            var draft = new StockMovementDraft(
                    "", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockId.class);
        }

        @Test
        @DisplayName("should fail when articleId is blank")
        void shouldFailWhenArticleIdBlank() {
            var draft = new StockMovementDraft(
                    "stock-1", "", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidArticleId.class);
        }

        @Test
        @DisplayName("should fail when articleId is null")
        void shouldFailWhenArticleIdNull() {
            var draft = new StockMovementDraft(
                    "stock-1", null, "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidArticleId.class);
        }

        @Test
        @DisplayName("should fail when stockBatchId is blank")
        void shouldFailWhenStockBatchIdBlank() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockBatchId.class);
        }

        @Test
        @DisplayName("should fail when stockBatchId is null")
        void shouldFailWhenStockBatchIdNull() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", null,
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockBatchId.class);
        }

        @Test
        @DisplayName("should fail when batchId is null")
        void shouldFailWhenBatchIdNull() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    null, "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class);
        }

        @Test
        @DisplayName("should fail when batchId is blank")
        void shouldFailWhenBatchIdBlank() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class);
        }

        @Test
        @DisplayName("should fail when batchType is null")
        void shouldFailWhenBatchTypeNull() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", null,
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class);
        }

        @Test
        @DisplayName("should fail when batchType is invalid")
        void shouldFailWhenBatchTypeInvalid() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "INVALID",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class);
        }

        @Test
        @DisplayName("should fail when movementType is null")
        void shouldFailWhenMovementTypeNull() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    null, null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidMovementType.class);
        }

        @Test
        @DisplayName("should fail when movementType is invalid")
        void shouldFailWhenMovementTypeInvalid() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "INVALID_TYPE", null,
                    "10.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidMovementType.class);
        }

        @Test
        @DisplayName("should fail when ADJUSTMENT has blank direction")
        void shouldFailWhenAdjustmentHasBlankDirection() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "ADJUSTMENT", "  ",
                    "10.0", "KILOGRAM",
                    "Correction", null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidDirection.class);
        }

        @Test
        @DisplayName("should fail when ADJUSTMENT has no direction")
        void shouldFailWhenAdjustmentHasNoDirection() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "ADJUSTMENT", null,
                    "10.0", "KILOGRAM",
                    "Correction", null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidDirection.class);
        }

        @Test
        @DisplayName("should fail when ADJUSTMENT has invalid direction")
        void shouldFailWhenAdjustmentHasInvalidDirection() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "ADJUSTMENT", "SIDEWAYS",
                    "10.0", "KILOGRAM",
                    "Correction", null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidDirection.class);
        }

        @Test
        @DisplayName("should fail when quantityAmount is null")
        void shouldFailWhenQuantityAmountNull() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    null, "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when quantityUnit is null")
        void shouldFailWhenQuantityUnitNull() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", null,
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when quantity is negative")
        void shouldFailWhenQuantityNegative() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "-5.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when quantity is zero")
        void shouldFailWhenQuantityZero() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when quantityAmount is not a number")
        void shouldFailWhenQuantityAmountNotNumber() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "abc", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when quantityUnit is invalid")
        void shouldFailWhenQuantityUnitInvalid() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "INVALID_UNIT",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when WASTE has no reason")
        void shouldFailWhenWasteHasNoReason() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "WASTE", null,
                    "5.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.ReasonRequired.class);
        }

        @Test
        @DisplayName("should fail when WASTE has blank reason")
        void shouldFailWhenWasteHasBlankReason() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "WASTE", null,
                    "5.0", "KILOGRAM",
                    "  ", null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.ReasonRequired.class);
        }

        @Test
        @DisplayName("should fail when ADJUSTMENT has no reason")
        void shouldFailWhenAdjustmentHasNoReason() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "ADJUSTMENT", "IN",
                    "2.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.ReasonRequired.class);
        }

        @Test
        @DisplayName("should fail when INTER_BRANCH_TRANSFER has no referenceDocumentId")
        void shouldFailWhenTransferHasNoReferenceDocument() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "INTER_BRANCH_TRANSFER", null,
                    "3.0", "KILOGRAM",
                    null, null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.ReferenceDocumentRequired.class);
        }

        @Test
        @DisplayName("should fail when INTER_BRANCH_TRANSFER has blank referenceDocumentId")
        void shouldFailWhenTransferHasBlankReferenceDocument() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "INTER_BRANCH_TRANSFER", null,
                    "3.0", "KILOGRAM",
                    null, "  ", "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.ReferenceDocumentRequired.class);
        }

        @Test
        @DisplayName("should fail when performedBy is null")
        void shouldFailWhenPerformedByNull() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, null
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidPerformedBy.class);
        }

        @Test
        @DisplayName("should fail when performedBy is blank")
        void shouldFailWhenPerformedByBlank() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "GOODS_RECEIPT", null,
                    "10.0", "KILOGRAM",
                    null, null, "  "
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidPerformedBy.class);
        }

        @Test
        @DisplayName("should fail when ADJUSTMENT has blank reason")
        void shouldFailWhenAdjustmentHasBlankReason() {
            var draft = new StockMovementDraft(
                    "stock-1", "article-1", "batch-id-1",
                    "CHARGE-001", "PRODUCED",
                    "ADJUSTMENT", "IN",
                    "2.0", "KILOGRAM",
                    "  ", null, "user-1"
            );
            var result = StockMovement.record(draft);
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.ReasonRequired.class);
        }
    }

    // ==================== reconstitute() ====================

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

        @Test
        @DisplayName("should reconstitute all fields from persistence")
        void shouldReconstituteAllFields() {
            var id = StockMovementId.generate();
            var stockId = StockId.of("stock-1");
            var articleId = ArticleId.of("article-1");
            var stockBatchId = StockBatchId.of("batch-id-1");
            var batchRef = new BatchReference("CHARGE-001", BatchType.PRODUCED);
            var quantity = Quantity.reconstitute(new BigDecimal("10.5"), UnitOfMeasure.KILOGRAM);
            var performedAt = Instant.now();

            var movement = StockMovement.reconstitute(
                    id, stockId, articleId, stockBatchId, batchRef,
                    MovementType.WASTE, MovementDirection.OUT, quantity,
                    "Expired", "REF-001", "user-1", performedAt
            );

            assertThat(movement.id()).isEqualTo(id);
            assertThat(movement.stockId()).isEqualTo(stockId);
            assertThat(movement.articleId()).isEqualTo(articleId);
            assertThat(movement.stockBatchId()).isEqualTo(stockBatchId);
            assertThat(movement.batchReference()).isEqualTo(batchRef);
            assertThat(movement.movementType()).isEqualTo(MovementType.WASTE);
            assertThat(movement.direction()).isEqualTo(MovementDirection.OUT);
            assertThat(movement.quantity()).isEqualTo(quantity);
            assertThat(movement.reason()).isEqualTo("Expired");
            assertThat(movement.referenceDocumentId()).isEqualTo("REF-001");
            assertThat(movement.performedBy()).isEqualTo("user-1");
            assertThat(movement.performedAt()).isEqualTo(performedAt);
            assertThat(movement.isOutgoing()).isTrue();
            assertThat(movement.isIncoming()).isFalse();
        }

        @Test
        @DisplayName("should reconstitute with null optional fields")
        void shouldReconstituteWithNullOptionals() {
            var movement = StockMovement.reconstitute(
                    StockMovementId.generate(),
                    StockId.of("stock-1"),
                    ArticleId.of("article-1"),
                    StockBatchId.of("batch-id-1"),
                    new BatchReference("CHARGE-001", BatchType.PURCHASED),
                    MovementType.GOODS_RECEIPT,
                    MovementDirection.IN,
                    Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.PIECE),
                    null, null, "user-1", Instant.now()
            );

            assertThat(movement.reason()).isNull();
            assertThat(movement.referenceDocumentId()).isNull();
            assertThat(movement.isIncoming()).isTrue();
        }
    }

    // ==================== equals/hashCode ====================

    @Nested
    @DisplayName("equals() and hashCode()")
    class Identity {

        @Test
        @DisplayName("movements with same ID are equal")
        void sameIdAreEqual() {
            var id = StockMovementId.generate();
            var performedAt = Instant.now();
            var qty = Quantity.reconstitute(new BigDecimal("1"), UnitOfMeasure.KILOGRAM);
            var batchRef = new BatchReference("C1", BatchType.PRODUCED);

            var m1 = StockMovement.reconstitute(id, StockId.of("s1"), ArticleId.of("a1"),
                    StockBatchId.of("b1"), batchRef, MovementType.SALE, MovementDirection.OUT,
                    qty, null, null, "u1", performedAt);
            var m2 = StockMovement.reconstitute(id, StockId.of("s2"), ArticleId.of("a2"),
                    StockBatchId.of("b2"), batchRef, MovementType.RETURN, MovementDirection.IN,
                    qty, null, null, "u2", performedAt);

            assertThat(m1).isEqualTo(m2);
            assertThat(m1.hashCode()).isEqualTo(m2.hashCode());
        }

        @Test
        @DisplayName("movements with different ID are not equal")
        void differentIdAreNotEqual() {
            var performedAt = Instant.now();
            var qty = Quantity.reconstitute(new BigDecimal("1"), UnitOfMeasure.KILOGRAM);
            var batchRef = new BatchReference("C1", BatchType.PRODUCED);

            var m1 = StockMovement.reconstitute(StockMovementId.generate(), StockId.of("s1"),
                    ArticleId.of("a1"), StockBatchId.of("b1"), batchRef,
                    MovementType.SALE, MovementDirection.OUT, qty, null, null, "u1", performedAt);
            var m2 = StockMovement.reconstitute(StockMovementId.generate(), StockId.of("s1"),
                    ArticleId.of("a1"), StockBatchId.of("b1"), batchRef,
                    MovementType.SALE, MovementDirection.OUT, qty, null, null, "u1", performedAt);

            assertThat(m1).isNotEqualTo(m2);
        }
    }
}
