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.util.List;

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

@DisplayName("Recipe Aggregate")
class RecipeTest {

    private RecipeDraft validDraft() {
        return new RecipeDraft(
                "Bratwurst Grob", 1, RecipeType.FINISHED_PRODUCT,
                "Grobe Bratwurst nach Hausrezept", 85,
                14, "100", "KILOGRAM", "article-123"
        );
    }

    private IngredientDraft validIngredientDraft(int position) {
        return new IngredientDraft(position, "article-123", "5.5", "KILOGRAM", null, false);
    }

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

        @Test
        @DisplayName("should create recipe in DRAFT status with valid inputs")
        void should_CreateInDraftStatus_When_ValidInputs() {
            var result = Recipe.create(validDraft());

            assertThat(result.isSuccess()).isTrue();
            var recipe = result.unsafeGetValue();
            assertThat(recipe.id()).isNotNull();
            assertThat(recipe.name().value()).isEqualTo("Bratwurst Grob");
            assertThat(recipe.version()).isEqualTo(1);
            assertThat(recipe.type()).isEqualTo(RecipeType.FINISHED_PRODUCT);
            assertThat(recipe.description()).isEqualTo("Grobe Bratwurst nach Hausrezept");
            assertThat(recipe.yieldPercentage().value()).isEqualTo(85);
            assertThat(recipe.shelfLifeDays()).isEqualTo(14);
            assertThat(recipe.outputQuantity().amount()).isEqualByComparingTo("100");
            assertThat(recipe.outputQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
            assertThat(recipe.status()).isEqualTo(RecipeStatus.DRAFT);
            assertThat(recipe.ingredients()).isEmpty();
            assertThat(recipe.productionSteps()).isEmpty();
            assertThat(recipe.createdAt()).isNotNull();
            assertThat(recipe.updatedAt()).isNotNull();
        }

        @Test
        @DisplayName("should fail when name is blank")
        void should_Fail_When_NameIsBlank() {
            var draft = new RecipeDraft(
                    "", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should fail when name is null")
        void should_Fail_When_NameIsNull() {
            var draft = new RecipeDraft(
                    null, 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should fail when version is less than 1")
        void should_Fail_When_VersionLessThanOne() {
            var draft = new RecipeDraft(
                    "Test", 0, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should fail when yield percentage is 0")
        void should_Fail_When_YieldPercentageIsZero() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.FINISHED_PRODUCT, null, 0, 14, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should fail when yield percentage is 201")
        void should_Fail_When_YieldPercentageIs201() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.FINISHED_PRODUCT, null, 201, 14, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should fail when shelf life is 0 for FINISHED_PRODUCT")
        void should_Fail_When_ShelfLifeZeroForFinishedProduct() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 0, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidShelfLife.class);
        }

        @Test
        @DisplayName("should fail when shelf life is null for FINISHED_PRODUCT")
        void should_Fail_When_ShelfLifeNullForFinishedProduct() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, null, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidShelfLife.class);
        }

        @Test
        @DisplayName("should fail when shelf life is 0 for INTERMEDIATE")
        void should_Fail_When_ShelfLifeZeroForIntermediate() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.INTERMEDIATE, null, 85, 0, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidShelfLife.class);
        }

        @Test
        @DisplayName("should allow null shelf life for RAW_MATERIAL")
        void should_AllowNullShelfLife_When_RawMaterial() {
            var draft = new RecipeDraft(
                    "Schweinefleisch", 1, RecipeType.RAW_MATERIAL, null, 100, null, "50", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should fail when output quantity is zero")
        void should_Fail_When_OutputQuantityZero() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "0", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should fail when output UoM is invalid")
        void should_Fail_When_OutputUomInvalid() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "INVALID", "article-123"
            );

            var result = Recipe.create(draft);

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

        @Test
        @DisplayName("should allow description to be null")
        void should_AllowNullDescription() {
            var draft = new RecipeDraft(
                    "Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
            );

            var result = Recipe.create(draft);

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

    @Nested
    @DisplayName("addIngredient()")
    class AddIngredient {

        @Test
        @DisplayName("should add ingredient to DRAFT recipe")
        void should_AddIngredient_When_DraftStatus() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            var result = recipe.addIngredient(validIngredientDraft(1));

            assertThat(result.isSuccess()).isTrue();
            var ingredient = result.unsafeGetValue();
            assertThat(ingredient.id()).isNotNull();
            assertThat(ingredient.position()).isEqualTo(1);
            assertThat(ingredient.articleId()).isEqualTo("article-123");
            assertThat(ingredient.quantity().amount()).isEqualByComparingTo("5.5");
            assertThat(ingredient.quantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
            assertThat(ingredient.substitutable()).isFalse();
            assertThat(recipe.ingredients()).hasSize(1);
        }

        @Test
        @DisplayName("should fail when recipe is ACTIVE")
        void should_Fail_When_ActiveStatus() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );

            var result = recipe.addIngredient(validIngredientDraft(1));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
        }

        @Test
        @DisplayName("should fail when position is duplicate")
        void should_Fail_When_DuplicatePosition() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            recipe.addIngredient(validIngredientDraft(1));

            var result = recipe.addIngredient(new IngredientDraft(1, "article-456", "3", "GRAM", null, false));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.DuplicatePosition.class);
        }

        @Test
        @DisplayName("should allow ingredient with subRecipeId")
        void should_AllowIngredient_WithSubRecipeId() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            var draft = new IngredientDraft(1, "article-123", "10", "KILOGRAM", "sub-recipe-id", true);

            var result = recipe.addIngredient(draft);

            assertThat(result.isSuccess()).isTrue();
            var ingredient = result.unsafeGetValue();
            assertThat(ingredient.subRecipeId()).isEqualTo("sub-recipe-id");
            assertThat(ingredient.substitutable()).isTrue();
        }

        @Test
        @DisplayName("should allow multiple ingredients with different positions")
        void should_AllowMultipleIngredients_WithDifferentPositions() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            recipe.addIngredient(validIngredientDraft(1));
            recipe.addIngredient(validIngredientDraft(2));
            recipe.addIngredient(validIngredientDraft(3));

            assertThat(recipe.ingredients()).hasSize(3);
        }
    }

    @Nested
    @DisplayName("removeIngredient()")
    class RemoveIngredient {

        @Test
        @DisplayName("should remove existing ingredient")
        void should_RemoveIngredient_When_Exists() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            var ingredient = recipe.addIngredient(validIngredientDraft(1)).unsafeGetValue();

            var result = recipe.removeIngredient(ingredient.id());

            assertThat(result.isSuccess()).isTrue();
            assertThat(recipe.ingredients()).isEmpty();
        }

        @Test
        @DisplayName("should fail when ingredient not found")
        void should_Fail_When_IngredientNotFound() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            var result = recipe.removeIngredient(IngredientId.generate());

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.IngredientNotFound.class);
        }

        @Test
        @DisplayName("should fail when recipe is not in DRAFT status")
        void should_Fail_When_NotDraftStatus() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );

            var result = recipe.removeIngredient(IngredientId.generate());

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
        }

        @Test
        @DisplayName("should allow gaps in positions after removal")
        void should_AllowPositionGaps_AfterRemoval() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            recipe.addIngredient(validIngredientDraft(1));
            var ingredient2 = recipe.addIngredient(validIngredientDraft(2)).unsafeGetValue();
            recipe.addIngredient(validIngredientDraft(3));

            recipe.removeIngredient(ingredient2.id());

            assertThat(recipe.ingredients()).hasSize(2);
            assertThat(recipe.ingredients().stream().map(i -> i.position()).toList())
                    .containsExactly(1, 3);
        }
    }

    @Nested
    @DisplayName("addProductionStep()")
    class AddProductionStep {

        @Test
        @DisplayName("should add production step to DRAFT recipe")
        void should_AddStep_When_DraftStatus() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            var result = recipe.addProductionStep(new ProductionStepDraft(1, "Fleisch wolfen", 15, 4));

            assertThat(result.isSuccess()).isTrue();
            var step = result.unsafeGetValue();
            assertThat(step.id()).isNotNull();
            assertThat(step.stepNumber()).isEqualTo(1);
            assertThat(step.description()).isEqualTo("Fleisch wolfen");
            assertThat(step.durationMinutes()).isEqualTo(15);
            assertThat(step.temperatureCelsius()).isEqualTo(4);
            assertThat(recipe.productionSteps()).hasSize(1);
        }

        @Test
        @DisplayName("should fail when recipe is ACTIVE")
        void should_Fail_When_ActiveStatus() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );

            var result = recipe.addProductionStep(new ProductionStepDraft(1, "Mischen", null, null));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
        }

        @Test
        @DisplayName("should fail when step number is duplicate")
        void should_Fail_When_DuplicateStepNumber() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            recipe.addProductionStep(new ProductionStepDraft(1, "Fleisch wolfen", null, null));

            var result = recipe.addProductionStep(new ProductionStepDraft(1, "Nochmal wolfen", null, null));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.DuplicateStepNumber.class);
        }

        @Test
        @DisplayName("should allow multiple steps with different numbers")
        void should_AllowMultipleSteps_WithDifferentNumbers() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            recipe.addProductionStep(new ProductionStepDraft(1, "Wolfen", 15, null));
            recipe.addProductionStep(new ProductionStepDraft(2, "Mischen", 10, null));
            recipe.addProductionStep(new ProductionStepDraft(3, "Füllen", 20, null));

            assertThat(recipe.productionSteps()).hasSize(3);
        }
    }

    @Nested
    @DisplayName("removeProductionStep()")
    class RemoveProductionStep {

        @Test
        @DisplayName("should remove existing step by step number")
        void should_RemoveStep_When_Exists() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            recipe.addProductionStep(new ProductionStepDraft(1, "Wolfen", null, null));

            var result = recipe.removeProductionStep(1);

            assertThat(result.isSuccess()).isTrue();
            assertThat(recipe.productionSteps()).isEmpty();
        }

        @Test
        @DisplayName("should fail when step not found")
        void should_Fail_When_StepNotFound() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            var result = recipe.removeProductionStep(99);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.StepNotFound.class);
        }

        @Test
        @DisplayName("should fail when recipe is not in DRAFT status")
        void should_Fail_When_NotDraftStatus() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );

            var result = recipe.removeProductionStep(1);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
        }

        @Test
        @DisplayName("should allow gaps in step numbers after removal")
        void should_AllowStepNumberGaps_AfterRemoval() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            recipe.addProductionStep(new ProductionStepDraft(1, "Wolfen", null, null));
            recipe.addProductionStep(new ProductionStepDraft(2, "Mischen", null, null));
            recipe.addProductionStep(new ProductionStepDraft(3, "Füllen", null, null));

            recipe.removeProductionStep(2);

            assertThat(recipe.productionSteps()).hasSize(2);
            assertThat(recipe.productionSteps().stream().map(s -> s.stepNumber()).toList())
                    .containsExactly(1, 3);
        }
    }

    @Nested
    @DisplayName("activate()")
    class Activate {

        @Test
        @DisplayName("should activate DRAFT recipe with ingredients")
        void should_Activate_When_DraftWithIngredients() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();
            recipe.addIngredient(validIngredientDraft(1));
            var updatedBefore = recipe.updatedAt();

            var result = recipe.activate();

            assertThat(result.isSuccess()).isTrue();
            assertThat(recipe.status()).isEqualTo(RecipeStatus.ACTIVE);
            assertThat(recipe.updatedAt()).isAfter(updatedBefore);
        }

        @Test
        @DisplayName("should fail when DRAFT recipe has no ingredients")
        void should_Fail_When_NoIngredients() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            var result = recipe.activate();

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NoIngredients.class);
        }

        @Test
        @DisplayName("should fail when recipe is already ACTIVE")
        void should_Fail_When_AlreadyActive() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );

            var result = recipe.activate();

            assertThat(result.isFailure()).isTrue();
            var error = result.unsafeGetError();
            assertThat(error).isInstanceOf(RecipeError.InvalidStatusTransition.class);
            var transition = (RecipeError.InvalidStatusTransition) error;
            assertThat(transition.current()).isEqualTo(RecipeStatus.ACTIVE);
            assertThat(transition.target()).isEqualTo(RecipeStatus.ACTIVE);
        }

        @Test
        @DisplayName("should fail when recipe is ARCHIVED")
        void should_Fail_When_Archived() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ARCHIVED, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );

            var result = recipe.activate();

            assertThat(result.isFailure()).isTrue();
            var error = result.unsafeGetError();
            assertThat(error).isInstanceOf(RecipeError.InvalidStatusTransition.class);
            var transition = (RecipeError.InvalidStatusTransition) error;
            assertThat(transition.current()).isEqualTo(RecipeStatus.ARCHIVED);
            assertThat(transition.target()).isEqualTo(RecipeStatus.ACTIVE);
        }
    }

    @Nested
    @DisplayName("archive()")
    class Archive {

        @Test
        @DisplayName("should archive ACTIVE recipe")
        void should_Archive_When_ActiveStatus() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );
            var updatedBefore = recipe.updatedAt();

            var result = recipe.archive();

            assertThat(result.isSuccess()).isTrue();
            assertThat(recipe.status()).isEqualTo(RecipeStatus.ARCHIVED);
            assertThat(recipe.updatedAt()).isAfter(updatedBefore);
        }

        @Test
        @DisplayName("should fail when recipe is in DRAFT status")
        void should_Fail_When_DraftStatus() {
            var recipe = Recipe.create(validDraft()).unsafeGetValue();

            var result = recipe.archive();

            assertThat(result.isFailure()).isTrue();
            var error = result.unsafeGetError();
            assertThat(error).isInstanceOf(RecipeError.InvalidStatusTransition.class);
            var transition = (RecipeError.InvalidStatusTransition) error;
            assertThat(transition.current()).isEqualTo(RecipeStatus.DRAFT);
            assertThat(transition.target()).isEqualTo(RecipeStatus.ARCHIVED);
        }

        @Test
        @DisplayName("should fail when recipe is already ARCHIVED")
        void should_Fail_When_AlreadyArchived() {
            var recipe = Recipe.reconstitute(
                    RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
                    null, new YieldPercentage(85), 14,
                    Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ARCHIVED, List.of(), List.of(),
                    java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
            );

            var result = recipe.archive();

            assertThat(result.isFailure()).isTrue();
            var error = result.unsafeGetError();
            assertThat(error).isInstanceOf(RecipeError.InvalidStatusTransition.class);
            var transition = (RecipeError.InvalidStatusTransition) error;
            assertThat(transition.current()).isEqualTo(RecipeStatus.ARCHIVED);
            assertThat(transition.target()).isEqualTo(RecipeStatus.ARCHIVED);
        }
    }

    @Nested
    @DisplayName("Equality")
    class Equality {

        @Test
        @DisplayName("should be equal when same ID")
        void should_BeEqual_When_SameId() {
            var recipe1 = Recipe.create(validDraft()).unsafeGetValue();
            var recipe2 = Recipe.reconstitute(
                    recipe1.id(), new RecipeName("Other"), 2, RecipeType.RAW_MATERIAL,
                    null, new YieldPercentage(100), null,
                    Quantity.of(new java.math.BigDecimal("50"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                    "article-123", RecipeStatus.ACTIVE, List.of(), List.of(), recipe1.createdAt(), recipe1.updatedAt()
            );

            assertThat(recipe1).isEqualTo(recipe2);
            assertThat(recipe1.hashCode()).isEqualTo(recipe2.hashCode());
        }

        @Test
        @DisplayName("should not be equal when different ID")
        void should_NotBeEqual_When_DifferentId() {
            var recipe1 = Recipe.create(validDraft()).unsafeGetValue();
            var recipe2 = Recipe.create(validDraft()).unsafeGetValue();

            assertThat(recipe1).isNotEqualTo(recipe2);
        }
    }
}
